dreamboard 0.1.21 → 0.1.22

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 (186) hide show
  1. package/dist/{chunk-EYYWGWTO.js → chunk-2RCUHMGL.js} +343 -1911
  2. package/dist/chunk-2RCUHMGL.js.map +1 -0
  3. package/dist/{chunk-MOVHYB6E.js → chunk-AQ5UUNJS.js} +1018 -692
  4. package/dist/chunk-AQ5UUNJS.js.map +1 -0
  5. package/dist/{chunk-BMYC6772.js → chunk-H3O43F5P.js} +9765 -3068
  6. package/dist/chunk-H3O43F5P.js.map +1 -0
  7. package/dist/dev-host/dev-api-proxy-plugin.ts +330 -0
  8. package/dist/dev-host/dev-diagnostics.ts +62 -0
  9. package/dist/dev-host/dev-fallback-stylesheet.ts +50 -0
  10. package/dist/dev-host/dev-host-controller.ts +686 -0
  11. package/dist/dev-host/dev-host-player-query.ts +17 -0
  12. package/dist/dev-host/dev-host-session-transport.ts +52 -0
  13. package/dist/dev-host/dev-host-storage.ts +56 -0
  14. package/dist/dev-host/dev-log-relay-plugin.ts +469 -0
  15. package/dist/dev-host/dev-runtime-config.ts +14 -0
  16. package/dist/dev-host/dev-runtime-platform.ts +419 -0
  17. package/dist/dev-host/dev-virtual-modules-plugin.ts +63 -0
  18. package/dist/dev-host/host-main.css +182 -0
  19. package/dist/dev-host/host-main.tsx +754 -0
  20. package/dist/dev-host/index.html +56 -0
  21. package/dist/dev-host/plugin-main.ts +55 -0
  22. package/dist/dev-host/plugin.html +24 -0
  23. package/dist/dev-host/start-dev-server.ts +138 -0
  24. package/dist/dev-host/virtual-modules.d.ts +27 -0
  25. package/dist/dist-FEPN3BDN.js +16543 -0
  26. package/dist/dist-FEPN3BDN.js.map +1 -0
  27. package/dist/{dist-WJRJNFLI.js → dist-FRURQI7Q.js} +4 -2
  28. package/dist/index.js +3 -3
  29. package/dist/internal.js +159 -124
  30. package/dist/internal.js.map +1 -1
  31. package/dist/runtime-packages/tailwind-config/shared-styles.css +146 -0
  32. package/dist/runtime-packages/ui/src/components/.gitkeep +0 -0
  33. package/dist/runtime-packages/ui/src/components/accordion.tsx +66 -0
  34. package/dist/runtime-packages/ui/src/components/ai-elements/actions.tsx +65 -0
  35. package/dist/runtime-packages/ui/src/components/ai-elements/artifact.tsx +147 -0
  36. package/dist/runtime-packages/ui/src/components/ai-elements/branch.tsx +215 -0
  37. package/dist/runtime-packages/ui/src/components/ai-elements/canvas.tsx +22 -0
  38. package/dist/runtime-packages/ui/src/components/ai-elements/chain-of-thought.tsx +228 -0
  39. package/dist/runtime-packages/ui/src/components/ai-elements/code-block.tsx +179 -0
  40. package/dist/runtime-packages/ui/src/components/ai-elements/confirmation.tsx +158 -0
  41. package/dist/runtime-packages/ui/src/components/ai-elements/connection.tsx +28 -0
  42. package/dist/runtime-packages/ui/src/components/ai-elements/context.tsx +408 -0
  43. package/dist/runtime-packages/ui/src/components/ai-elements/controls.tsx +18 -0
  44. package/dist/runtime-packages/ui/src/components/ai-elements/conversation.tsx +97 -0
  45. package/dist/runtime-packages/ui/src/components/ai-elements/edge.tsx +140 -0
  46. package/dist/runtime-packages/ui/src/components/ai-elements/image.tsx +24 -0
  47. package/dist/runtime-packages/ui/src/components/ai-elements/inline-citation.tsx +287 -0
  48. package/dist/runtime-packages/ui/src/components/ai-elements/loader.tsx +96 -0
  49. package/dist/runtime-packages/ui/src/components/ai-elements/message.tsx +463 -0
  50. package/dist/runtime-packages/ui/src/components/ai-elements/node.tsx +71 -0
  51. package/dist/runtime-packages/ui/src/components/ai-elements/open-in-chat.tsx +365 -0
  52. package/dist/runtime-packages/ui/src/components/ai-elements/panel.tsx +15 -0
  53. package/dist/runtime-packages/ui/src/components/ai-elements/plan.tsx +142 -0
  54. package/dist/runtime-packages/ui/src/components/ai-elements/prompt-input.tsx +1380 -0
  55. package/dist/runtime-packages/ui/src/components/ai-elements/queue.tsx +274 -0
  56. package/dist/runtime-packages/ui/src/components/ai-elements/reasoning.tsx +182 -0
  57. package/dist/runtime-packages/ui/src/components/ai-elements/response.tsx +22 -0
  58. package/dist/runtime-packages/ui/src/components/ai-elements/shimmer.tsx +64 -0
  59. package/dist/runtime-packages/ui/src/components/ai-elements/sources.tsx +77 -0
  60. package/dist/runtime-packages/ui/src/components/ai-elements/suggestion.tsx +53 -0
  61. package/dist/runtime-packages/ui/src/components/ai-elements/task.tsx +87 -0
  62. package/dist/runtime-packages/ui/src/components/ai-elements/tool.tsx +165 -0
  63. package/dist/runtime-packages/ui/src/components/ai-elements/toolbar.tsx +16 -0
  64. package/dist/runtime-packages/ui/src/components/ai-elements/web-preview.tsx +263 -0
  65. package/dist/runtime-packages/ui/src/components/alert-dialog.tsx +157 -0
  66. package/dist/runtime-packages/ui/src/components/alert.tsx +66 -0
  67. package/dist/runtime-packages/ui/src/components/avatar.tsx +53 -0
  68. package/dist/runtime-packages/ui/src/components/badge.tsx +46 -0
  69. package/dist/runtime-packages/ui/src/components/button-group.tsx +83 -0
  70. package/dist/runtime-packages/ui/src/components/button.tsx +65 -0
  71. package/dist/runtime-packages/ui/src/components/card.tsx +92 -0
  72. package/dist/runtime-packages/ui/src/components/carousel.tsx +241 -0
  73. package/dist/runtime-packages/ui/src/components/collapsible.tsx +33 -0
  74. package/dist/runtime-packages/ui/src/components/command.tsx +184 -0
  75. package/dist/runtime-packages/ui/src/components/context-menu.tsx +252 -0
  76. package/dist/runtime-packages/ui/src/components/counter.tsx +11 -0
  77. package/dist/runtime-packages/ui/src/components/dialog.tsx +146 -0
  78. package/dist/runtime-packages/ui/src/components/drawer.tsx +132 -0
  79. package/dist/runtime-packages/ui/src/components/dropdown-menu.tsx +257 -0
  80. package/dist/runtime-packages/ui/src/components/form.tsx +168 -0
  81. package/dist/runtime-packages/ui/src/components/header.tsx +13 -0
  82. package/dist/runtime-packages/ui/src/components/hover-card.tsx +44 -0
  83. package/dist/runtime-packages/ui/src/components/input-group.tsx +178 -0
  84. package/dist/runtime-packages/ui/src/components/input.tsx +21 -0
  85. package/dist/runtime-packages/ui/src/components/item.tsx +193 -0
  86. package/dist/runtime-packages/ui/src/components/label.tsx +24 -0
  87. package/dist/runtime-packages/ui/src/components/navigation-menu.tsx +168 -0
  88. package/dist/runtime-packages/ui/src/components/popover.tsx +55 -0
  89. package/dist/runtime-packages/ui/src/components/progress.tsx +31 -0
  90. package/dist/runtime-packages/ui/src/components/radio-group.tsx +45 -0
  91. package/dist/runtime-packages/ui/src/components/resizable.tsx +56 -0
  92. package/dist/runtime-packages/ui/src/components/scroll-area.tsx +58 -0
  93. package/dist/runtime-packages/ui/src/components/select.tsx +187 -0
  94. package/dist/runtime-packages/ui/src/components/separator.tsx +28 -0
  95. package/dist/runtime-packages/ui/src/components/skeleton.tsx +13 -0
  96. package/dist/runtime-packages/ui/src/components/sonner.tsx +25 -0
  97. package/dist/runtime-packages/ui/src/components/spinner.tsx +16 -0
  98. package/dist/runtime-packages/ui/src/components/switch.tsx +31 -0
  99. package/dist/runtime-packages/ui/src/components/table.tsx +137 -0
  100. package/dist/runtime-packages/ui/src/components/tabs.tsx +74 -0
  101. package/dist/runtime-packages/ui/src/components/textarea.tsx +23 -0
  102. package/dist/runtime-packages/ui/src/components/theme-provider.tsx +73 -0
  103. package/dist/runtime-packages/ui/src/components/tooltip.tsx +61 -0
  104. package/dist/runtime-packages/ui/src/components/ui/file-upload.tsx +195 -0
  105. package/dist/runtime-packages/ui/src/hooks/.gitkeep +0 -0
  106. package/dist/runtime-packages/ui/src/index.ts +68 -0
  107. package/dist/runtime-packages/ui/src/lib/utils.ts +6 -0
  108. package/dist/runtime-packages/ui-host-runtime/src/actor-principal.ts +68 -0
  109. package/dist/runtime-packages/ui-host-runtime/src/components/host-controls.tsx +359 -0
  110. package/dist/runtime-packages/ui-host-runtime/src/components/host-feedback-toaster.tsx +274 -0
  111. package/dist/runtime-packages/ui-host-runtime/src/components/host-feedback.tsx +214 -0
  112. package/dist/runtime-packages/ui-host-runtime/src/components/host-session-metadata.tsx +135 -0
  113. package/dist/runtime-packages/ui-host-runtime/src/components/index.ts +5 -0
  114. package/dist/runtime-packages/ui-host-runtime/src/components/perf-overlay.tsx +194 -0
  115. package/dist/runtime-packages/ui-host-runtime/src/host-controls.tsx +1 -0
  116. package/dist/runtime-packages/ui-host-runtime/src/host-feedback.tsx +1 -0
  117. package/dist/runtime-packages/ui-host-runtime/src/host-session-transport.ts +162 -0
  118. package/dist/runtime-packages/ui-host-runtime/src/index.ts +2 -0
  119. package/dist/runtime-packages/ui-host-runtime/src/logger.ts +11 -0
  120. package/dist/runtime-packages/ui-host-runtime/src/perf.ts +253 -0
  121. package/dist/runtime-packages/ui-host-runtime/src/plugin-bridge.ts +195 -0
  122. package/dist/runtime-packages/ui-host-runtime/src/plugin-health-check.ts +138 -0
  123. package/dist/runtime-packages/ui-host-runtime/src/plugin-messages.ts +159 -0
  124. package/dist/runtime-packages/ui-host-runtime/src/plugin-session-gateway.ts +524 -0
  125. package/dist/runtime-packages/ui-host-runtime/src/runtime/index.ts +12 -0
  126. package/dist/runtime-packages/ui-host-runtime/src/screenshot/projection-to-snapshot.ts +122 -0
  127. package/dist/runtime-packages/ui-host-runtime/src/screenshot/static-store-api.ts +26 -0
  128. package/dist/runtime-packages/ui-host-runtime/src/session-ingress-controller.ts +477 -0
  129. package/dist/runtime-packages/ui-host-runtime/src/session-ingress.ts +209 -0
  130. package/dist/runtime-packages/ui-host-runtime/src/session-live-runtime.ts +112 -0
  131. package/dist/runtime-packages/ui-host-runtime/src/session-model.ts +318 -0
  132. package/dist/runtime-packages/ui-host-runtime/src/session-projection.ts +211 -0
  133. package/dist/runtime-packages/ui-host-runtime/src/session-state-reducer.ts +814 -0
  134. package/dist/runtime-packages/ui-host-runtime/src/sse-manager.ts +334 -0
  135. package/dist/runtime-packages/ui-host-runtime/src/unified-session-store.ts +180 -0
  136. package/dist/scaffold/assets/static/app/tsconfig.framework.json +3 -1
  137. package/dist/scaffold/assets/static/ui/tsconfig.framework.json +7 -19
  138. package/package.json +52 -17
  139. package/skills/dreamboard/SKILL.md +41 -75
  140. package/LICENSE +0 -89
  141. package/NOTICE +0 -1
  142. package/dist/chunk-BMYC6772.js.map +0 -1
  143. package/dist/chunk-EYYWGWTO.js.map +0 -1
  144. package/dist/chunk-MOVHYB6E.js.map +0 -1
  145. package/dist/scaffold/assets/static/ui/App.tsx +0 -22
  146. package/dist/src-CUL7EGGG.js +0 -634
  147. package/dist/src-CUL7EGGG.js.map +0 -1
  148. package/skills/dreamboard/references/authoring-lifecycle.md +0 -102
  149. package/skills/dreamboard/references/board-surfaces.md +0 -36
  150. package/skills/dreamboard/references/board-topology.md +0 -443
  151. package/skills/dreamboard/references/boards-and-topology.md +0 -100
  152. package/skills/dreamboard/references/card-actions.md +0 -107
  153. package/skills/dreamboard/references/cli.md +0 -120
  154. package/skills/dreamboard/references/core-concepts.md +0 -514
  155. package/skills/dreamboard/references/custom-renderers.md +0 -26
  156. package/skills/dreamboard/references/derived-values.md +0 -55
  157. package/skills/dreamboard/references/effects.md +0 -111
  158. package/skills/dreamboard/references/game-contract.md +0 -89
  159. package/skills/dreamboard/references/game-definition.md +0 -89
  160. package/skills/dreamboard/references/game-shell.md +0 -80
  161. package/skills/dreamboard/references/hand-surfaces.md +0 -33
  162. package/skills/dreamboard/references/index.md +0 -112
  163. package/skills/dreamboard/references/inputs-and-targets.md +0 -160
  164. package/skills/dreamboard/references/interactions.md +0 -158
  165. package/skills/dreamboard/references/manifest-fields.md +0 -137
  166. package/skills/dreamboard/references/manifest.md +0 -81
  167. package/skills/dreamboard/references/package-surfaces.md +0 -69
  168. package/skills/dreamboard/references/phases.md +0 -145
  169. package/skills/dreamboard/references/prompts-and-choices.md +0 -55
  170. package/skills/dreamboard/references/quickstart.md +0 -71
  171. package/skills/dreamboard/references/rule-authoring.md +0 -144
  172. package/skills/dreamboard/references/setup-bootstrap.md +0 -78
  173. package/skills/dreamboard/references/stages-and-zones.md +0 -59
  174. package/skills/dreamboard/references/static-views.md +0 -67
  175. package/skills/dreamboard/references/table-queries-and-ops.md +0 -103
  176. package/skills/dreamboard/references/testing-bases.md +0 -147
  177. package/skills/dreamboard/references/testing-generated-contracts.md +0 -142
  178. package/skills/dreamboard/references/testing-runtime-assertions.md +0 -124
  179. package/skills/dreamboard/references/testing-scenarios.md +0 -148
  180. package/skills/dreamboard/references/testing-ui-tests.md +0 -174
  181. package/skills/dreamboard/references/testing.md +0 -161
  182. package/skills/dreamboard/references/ui-architecture.md +0 -137
  183. package/skills/dreamboard/references/ui-components.md +0 -34
  184. package/skills/dreamboard/references/views.md +0 -72
  185. package/skills/dreamboard/references/workspace-layout.md +0 -136
  186. /package/dist/{dist-WJRJNFLI.js.map → dist-FRURQI7Q.js.map} +0 -0
@@ -0,0 +1,359 @@
1
+ import { useMemo, useRef, useState } from "react";
2
+ import { cn } from "@dreamboard/ui/lib/utils";
3
+ import { Button } from "@dreamboard/ui/components/button";
4
+ import { Badge } from "@dreamboard/ui/components/badge";
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuLabel,
9
+ DropdownMenuRadioGroup,
10
+ DropdownMenuRadioItem,
11
+ DropdownMenuSeparator,
12
+ DropdownMenuTrigger,
13
+ } from "@dreamboard/ui/components/dropdown-menu";
14
+ import {
15
+ Popover,
16
+ PopoverContent,
17
+ PopoverTrigger,
18
+ } from "@dreamboard/ui/components/popover";
19
+ import { ScrollArea } from "@dreamboard/ui/components/scroll-area";
20
+ import {
21
+ Check,
22
+ ChevronDown,
23
+ Clock3,
24
+ History,
25
+ RotateCcw,
26
+ Sparkles,
27
+ Users,
28
+ } from "lucide-react";
29
+ import type { HistoryState } from "../unified-session-store.js";
30
+
31
+ export interface HostControllablePlayer {
32
+ playerId: string;
33
+ displayName: string;
34
+ }
35
+
36
+ export interface HostPlayerSwitcherProps {
37
+ controllablePlayers: HostControllablePlayer[];
38
+ controllingPlayerId: string | null;
39
+ onSwitchPlayer: (playerId: string) => void;
40
+ className?: string;
41
+ }
42
+
43
+ export interface HostHistoryNavigatorProps {
44
+ isHost: boolean;
45
+ history: HistoryState | null;
46
+ onRestoreHistory: (entryId: string) => Promise<void> | void;
47
+ className?: string;
48
+ }
49
+
50
+ export interface HostSessionToolbarProps {
51
+ children: React.ReactNode;
52
+ className?: string;
53
+ }
54
+
55
+ function formatHistoryTimestamp(timestamp: string): string {
56
+ try {
57
+ return new Date(timestamp).toLocaleTimeString(undefined, {
58
+ hour: "2-digit",
59
+ minute: "2-digit",
60
+ second: "2-digit",
61
+ });
62
+ } catch {
63
+ return timestamp;
64
+ }
65
+ }
66
+
67
+ export function HostSessionToolbar({
68
+ children,
69
+ className,
70
+ }: HostSessionToolbarProps) {
71
+ return (
72
+ <div
73
+ className={cn("flex flex-wrap items-center justify-end gap-2", className)}
74
+ >
75
+ {children}
76
+ </div>
77
+ );
78
+ }
79
+
80
+ export function HostPlayerSwitcher({
81
+ controllablePlayers,
82
+ controllingPlayerId,
83
+ onSwitchPlayer,
84
+ className,
85
+ }: HostPlayerSwitcherProps) {
86
+ const currentPlayer = useMemo(
87
+ () =>
88
+ controllablePlayers.find(
89
+ (player) => player.playerId === controllingPlayerId,
90
+ ) ?? controllablePlayers[0],
91
+ [controllablePlayers, controllingPlayerId],
92
+ );
93
+
94
+ if (controllablePlayers.length <= 1) {
95
+ return null;
96
+ }
97
+
98
+ // Designer's Notebook: the trigger button is the ONE punctuation moment
99
+ // for this control (wobbly + hard shadow). The dropdown contents below are
100
+ // intentionally calm — plain rows, no rotation, no per-row wobbly borders.
101
+ return (
102
+ <DropdownMenu>
103
+ <DropdownMenuTrigger asChild>
104
+ <Button
105
+ variant="outline"
106
+ className={cn(
107
+ "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",
108
+ className,
109
+ )}
110
+ >
111
+ <span className="flex min-w-0 items-center gap-3">
112
+ <span className="flex h-10 w-10 shrink-0 items-center justify-center border-2 border-border bg-[#e7eefc] text-[#2d5da1]">
113
+ <Users className="h-4 w-4 text-[#2d5da1]" />
114
+ </span>
115
+ <span className="min-w-0">
116
+ <span className="block text-[10px] font-bold uppercase tracking-[0.18em] text-muted-foreground">
117
+ Seat Control
118
+ </span>
119
+ <span className="block truncate font-display text-lg leading-none text-foreground">
120
+ {currentPlayer?.displayName ?? "Choose player"}
121
+ </span>
122
+ <span className="mt-1 block truncate text-xs text-muted-foreground">
123
+ {currentPlayer?.playerId ?? "Switch the active seat"}
124
+ </span>
125
+ </span>
126
+ </span>
127
+ <span className="flex shrink-0 items-center gap-2">
128
+ <Badge
129
+ variant="secondary"
130
+ className="border border-border/40 bg-[#efe7da] px-2.5 py-0.5 text-[10px] uppercase tracking-[0.14em] text-foreground shadow-none"
131
+ >
132
+ {controllablePlayers.length} seats
133
+ </Badge>
134
+ <ChevronDown className="h-4 w-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
135
+ </span>
136
+ </Button>
137
+ </DropdownMenuTrigger>
138
+ <DropdownMenuContent
139
+ align="end"
140
+ className="z-[80] w-[22rem] border-2 border-border bg-[#fdfbf7] p-2 font-sans"
141
+ >
142
+ <DropdownMenuLabel className="px-2 pb-3 pt-1">
143
+ <div className="flex items-start justify-between gap-3">
144
+ <div>
145
+ <p className="text-[10px] font-bold uppercase tracking-[0.18em] text-muted-foreground">
146
+ Play As
147
+ </p>
148
+ <p className="mt-1 font-display text-xl leading-none text-foreground">
149
+ Switch the active seat
150
+ </p>
151
+ </div>
152
+ <Sparkles className="h-5 w-5 text-primary shrink-0" />
153
+ </div>
154
+ </DropdownMenuLabel>
155
+ <DropdownMenuSeparator className="mx-1 border-b border-border/40 bg-transparent" />
156
+ <DropdownMenuRadioGroup
157
+ value={controllingPlayerId ?? ""}
158
+ onValueChange={(playerId) => {
159
+ if (playerId !== controllingPlayerId) {
160
+ onSwitchPlayer(playerId);
161
+ }
162
+ }}
163
+ >
164
+ {controllablePlayers.map((player, index) => (
165
+ <DropdownMenuRadioItem
166
+ key={player.playerId}
167
+ value={player.playerId}
168
+ 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"
169
+ >
170
+ <div className="flex min-w-0 items-center gap-3">
171
+ <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">
172
+ {index + 1}
173
+ </div>
174
+ <div className="min-w-0 flex-1">
175
+ <span className="block truncate font-display text-lg leading-none">
176
+ {player.displayName}
177
+ </span>
178
+ <span className="mt-1 block truncate text-xs text-muted-foreground">
179
+ {player.playerId}
180
+ </span>
181
+ </div>
182
+ {player.playerId === controllingPlayerId ? (
183
+ <Check className="ml-auto h-5 w-5 shrink-0 text-primary" />
184
+ ) : null}
185
+ </div>
186
+ </DropdownMenuRadioItem>
187
+ ))}
188
+ </DropdownMenuRadioGroup>
189
+ </DropdownMenuContent>
190
+ </DropdownMenu>
191
+ );
192
+ }
193
+
194
+ export function HostHistoryNavigator({
195
+ isHost,
196
+ history,
197
+ onRestoreHistory,
198
+ className,
199
+ }: HostHistoryNavigatorProps) {
200
+ const triggerContainerRef = useRef<HTMLDivElement | null>(null);
201
+ const [open, setOpen] = useState(false);
202
+ const [confirmEntryId, setConfirmEntryId] = useState<string | null>(null);
203
+ const [restoringEntryId, setRestoringEntryId] = useState<string | null>(null);
204
+
205
+ const entries = useMemo(
206
+ () => (history ? [...history.entries].reverse() : []),
207
+ [history],
208
+ );
209
+
210
+ if (!isHost || !history || history.entries.length === 0) {
211
+ return null;
212
+ }
213
+
214
+ const handleRestoreClick = async (entryId: string) => {
215
+ if (confirmEntryId !== entryId) {
216
+ setConfirmEntryId(entryId);
217
+ return;
218
+ }
219
+
220
+ setRestoringEntryId(entryId);
221
+ try {
222
+ await onRestoreHistory(entryId);
223
+ setOpen(false);
224
+ setConfirmEntryId(null);
225
+ } finally {
226
+ setRestoringEntryId(null);
227
+ }
228
+ };
229
+
230
+ const popoverContainer = (() => {
231
+ const container = triggerContainerRef.current?.closest(
232
+ "[data-slot='drawer-content']",
233
+ );
234
+ return container instanceof HTMLElement ? container : null;
235
+ })();
236
+
237
+ return (
238
+ <div ref={triggerContainerRef}>
239
+ <Popover
240
+ open={open}
241
+ onOpenChange={(nextOpen) => {
242
+ setOpen(nextOpen);
243
+ if (!nextOpen) {
244
+ setConfirmEntryId(null);
245
+ }
246
+ }}
247
+ >
248
+ <PopoverTrigger asChild>
249
+ {/* Calm secondary control — the player switcher is the loud one
250
+ in this toolbar; history is its quiet sibling. */}
251
+ <Button
252
+ variant="outline"
253
+ size="sm"
254
+ className={cn(
255
+ "h-10 gap-2 border-2 border-border bg-white text-foreground transition-colors hover:bg-[#e5e0d8]",
256
+ className,
257
+ )}
258
+ >
259
+ <History className="h-4 w-4 text-muted-foreground" />
260
+ <span className="hidden sm:inline">History</span>
261
+ <Badge
262
+ variant="secondary"
263
+ className="border border-border/40 bg-[#fff9c4] text-foreground"
264
+ >
265
+ {history.entries.length}
266
+ </Badge>
267
+ </Button>
268
+ </PopoverTrigger>
269
+ {/* Designer's Notebook: trigger above is the punctuation moment.
270
+ Popover contents are calm — plain bordered rows, no per-row
271
+ wobbly. Restore CTA only "presses" via translate, no inflation. */}
272
+ <PopoverContent
273
+ container={popoverContainer}
274
+ side="right"
275
+ align="start"
276
+ sideOffset={12}
277
+ collisionPadding={16}
278
+ style={{ zIndex: 200 }}
279
+ className="z-[200] w-[26rem] border-2 border-border bg-[#fdfbf7] p-0"
280
+ >
281
+ <div className="border-b-2 border-border bg-white px-4 py-3">
282
+ <div className="flex items-center gap-2 font-display text-base">
283
+ <History className="h-4 w-4" />
284
+ Session History
285
+ </div>
286
+ <p className="mt-1 font-sans text-xs text-muted-foreground">
287
+ Restore a previous game state. This affects the shared host
288
+ session.
289
+ </p>
290
+ </div>
291
+ <ScrollArea className="max-h-80">
292
+ <div className="space-y-2 p-3">
293
+ {entries.map((entry: HistoryState["entries"][number]) => {
294
+ const isConfirming = confirmEntryId === entry.id;
295
+ const isRestoring = restoringEntryId === entry.id;
296
+
297
+ return (
298
+ <div
299
+ key={entry.id}
300
+ className={cn(
301
+ "border-2 border-border px-3 py-3 transition-colors",
302
+ entry.isCurrent ? "bg-[#fff9c4]" : "bg-white",
303
+ )}
304
+ >
305
+ <div className="flex items-start justify-between gap-3">
306
+ <div className="min-w-0 space-y-1">
307
+ <div className="flex items-center gap-2">
308
+ <span className="truncate text-sm font-medium">
309
+ {entry.description}
310
+ </span>
311
+ {entry.isCurrent && (
312
+ <Badge
313
+ variant="secondary"
314
+ className="shrink-0 border border-border/40 bg-[#e5e0d8] text-foreground"
315
+ >
316
+ Current
317
+ </Badge>
318
+ )}
319
+ </div>
320
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
321
+ <Clock3 className="h-3.5 w-3.5" />
322
+ <span>{formatHistoryTimestamp(entry.timestamp)}</span>
323
+ <span>v{entry.version}</span>
324
+ </div>
325
+ </div>
326
+ {!entry.isCurrent && (
327
+ <Button
328
+ size="sm"
329
+ variant={isConfirming ? "default" : "outline"}
330
+ className="shrink-0"
331
+ disabled={isRestoring}
332
+ onClick={() => void handleRestoreClick(entry.id)}
333
+ >
334
+ {isRestoring ? (
335
+ "Restoring..."
336
+ ) : isConfirming ? (
337
+ <>
338
+ <Check className="mr-1 h-4 w-4" />
339
+ Confirm
340
+ </>
341
+ ) : (
342
+ <>
343
+ <RotateCcw className="mr-1 h-4 w-4" />
344
+ Restore
345
+ </>
346
+ )}
347
+ </Button>
348
+ )}
349
+ </div>
350
+ </div>
351
+ );
352
+ })}
353
+ </div>
354
+ </ScrollArea>
355
+ </PopoverContent>
356
+ </Popover>
357
+ </div>
358
+ );
359
+ }
@@ -0,0 +1,274 @@
1
+ import { useEffect } from "react";
2
+ import { Toaster, toast } from "sonner";
3
+ import { AlertTriangle, Bell, Clock3, X } from "lucide-react";
4
+ import {
5
+ intentForVariant,
6
+ surfaceStyle,
7
+ useTheme,
8
+ type ButtonVariant,
9
+ type Theme,
10
+ } from "@dreamboard/ui-sdk";
11
+ import type { HostFeedback } from "../unified-session-store.js";
12
+
13
+ export interface HostFeedbackToasterProps {
14
+ feedback?: HostFeedback[];
15
+ onDismiss?: (feedbackId: string) => void;
16
+ }
17
+
18
+ interface FeedbackPresentation {
19
+ title: string;
20
+ description: string;
21
+ duration: number;
22
+ variant: Extract<ButtonVariant, "danger" | "warning" | "success" | "info">;
23
+ icon: typeof AlertTriangle;
24
+ }
25
+
26
+ function describeFeedback(item: HostFeedback): FeedbackPresentation {
27
+ switch (item.type) {
28
+ case "YOUR_TURN": {
29
+ const activePlayerCount =
30
+ item.payload.type === "YOUR_TURN"
31
+ ? item.payload.activePlayers.length
32
+ : 0;
33
+ return {
34
+ title: "Your turn",
35
+ description:
36
+ activePlayerCount > 1
37
+ ? "You can act with one of your controlled players."
38
+ : "You can act now.",
39
+ duration: 3500,
40
+ variant: "success",
41
+ icon: Bell,
42
+ };
43
+ }
44
+ case "PROMPT_OPENED": {
45
+ const title =
46
+ item.payload.type === "PROMPT_OPENED" ? item.payload.title : undefined;
47
+ const targetPlayer =
48
+ item.payload.type === "PROMPT_OPENED"
49
+ ? item.payload.targetPlayer
50
+ : undefined;
51
+ return {
52
+ title: "Response needed",
53
+ description: targetPlayer
54
+ ? `${title ?? "A prompt is waiting."} (${targetPlayer})`
55
+ : (title ?? "A prompt is waiting."),
56
+ duration: 5000,
57
+ variant: "warning",
58
+ icon: Clock3,
59
+ };
60
+ }
61
+ case "ACTION_REJECTED": {
62
+ const reason =
63
+ item.payload.type === "ACTION_REJECTED"
64
+ ? item.payload.targetPlayer
65
+ ? `${item.payload.reason} (${item.payload.targetPlayer})`
66
+ : item.payload.reason
67
+ : "Action rejected.";
68
+ return {
69
+ title: "Action rejected",
70
+ description: reason,
71
+ duration: 5000,
72
+ variant: "danger",
73
+ icon: AlertTriangle,
74
+ };
75
+ }
76
+ }
77
+
78
+ throw new Error(`Unsupported host feedback item type: ${String(item.type)}`);
79
+ }
80
+
81
+ interface HostFeedbackToastBodyProps {
82
+ presentation: FeedbackPresentation;
83
+ theme: Theme;
84
+ onDismiss: () => void;
85
+ }
86
+
87
+ /**
88
+ * Inner toast body. Rendered through sonner's `toast.custom` so the
89
+ * surface is fully owned by the active {@link Theme} — no implicit
90
+ * `richColors` styling, no hardcoded `bg-*` Tailwind classes.
91
+ */
92
+ function HostFeedbackToastBody({
93
+ presentation,
94
+ theme,
95
+ onDismiss,
96
+ }: HostFeedbackToastBodyProps) {
97
+ const intent = intentForVariant(theme, presentation.variant);
98
+ const Icon = presentation.icon;
99
+ return (
100
+ <div
101
+ role="status"
102
+ aria-live="polite"
103
+ style={{
104
+ ...surfaceStyle(theme, { tone: "card", radius: "lg" }),
105
+ background: intent.soft,
106
+ color: intent.onSoft,
107
+ border: `1px solid ${intent.border}`,
108
+ boxShadow: theme.elevation.lifted,
109
+ display: "flex",
110
+ alignItems: "flex-start",
111
+ gap: theme.space[3],
112
+ padding: theme.space[3],
113
+ // Match sonner's default 356px so the toast lines up with
114
+ // sibling toasts that may live inside the same stack.
115
+ minWidth: 320,
116
+ maxWidth: 420,
117
+ fontFamily: theme.typography.fontFamily.body,
118
+ }}
119
+ >
120
+ <Icon
121
+ size={20}
122
+ strokeWidth={2.5}
123
+ aria-hidden="true"
124
+ style={{
125
+ flexShrink: 0,
126
+ marginTop: 2,
127
+ color: intent.solid,
128
+ }}
129
+ />
130
+ <div style={{ flex: 1, minWidth: 0 }}>
131
+ <div
132
+ style={{
133
+ fontFamily: theme.typography.fontFamily.display,
134
+ fontSize: theme.typography.fontSize.md,
135
+ fontWeight: theme.typography.fontWeight.bold,
136
+ lineHeight: theme.typography.lineHeight.tight,
137
+ color: intent.onSoft,
138
+ }}
139
+ >
140
+ {presentation.title}
141
+ </div>
142
+ <div
143
+ style={{
144
+ marginTop: theme.space[1],
145
+ fontSize: theme.typography.fontSize.sm,
146
+ fontWeight: theme.typography.fontWeight.medium,
147
+ lineHeight: theme.typography.lineHeight.normal,
148
+ color: intent.onSoft,
149
+ opacity: 0.92,
150
+ wordBreak: "break-word",
151
+ }}
152
+ >
153
+ {presentation.description}
154
+ </div>
155
+ </div>
156
+ <button
157
+ type="button"
158
+ aria-label="Dismiss notification"
159
+ onClick={onDismiss}
160
+ style={{
161
+ flexShrink: 0,
162
+ width: 28,
163
+ height: 28,
164
+ display: "inline-flex",
165
+ alignItems: "center",
166
+ justifyContent: "center",
167
+ background: "transparent",
168
+ border: "none",
169
+ borderRadius: theme.radius.pill,
170
+ color: intent.onSoft,
171
+ cursor: "pointer",
172
+ opacity: 0.7,
173
+ transition: `opacity ${theme.motion.duration.fast} ${theme.motion.easing.out}`,
174
+ }}
175
+ onMouseEnter={(event) => {
176
+ event.currentTarget.style.opacity = "1";
177
+ }}
178
+ onMouseLeave={(event) => {
179
+ event.currentTarget.style.opacity = "0.7";
180
+ }}
181
+ >
182
+ <X size={16} aria-hidden="true" />
183
+ </button>
184
+ </div>
185
+ );
186
+ }
187
+
188
+ /**
189
+ * Mounts a {@link Toaster} and dispatches host-feedback events as
190
+ * themed sonner toasts.
191
+ *
192
+ * Implementation notes:
193
+ *
194
+ * - We use `toast.custom(jsx, { id })` with a stable `id` so sonner
195
+ * deduplicates the same feedback id by itself. The previous
196
+ * implementation tracked processed ids in a `useRef`, which reset
197
+ * across remounts (StrictMode in dev, parent re-renders that swap
198
+ * the toaster's key) and caused the "shows / disappears / shows
199
+ * again" flicker. With sonner-owned dedup, remounts no longer
200
+ * replay the queue.
201
+ * - We render a fully themed body so the toast picks up the active
202
+ * `useTheme()` palette, font stack, and elevation tokens. Sonner's
203
+ * default `richColors` theme is intentionally not used because it
204
+ * produces a parallel non-themeable colour scheme.
205
+ * - Host feedback is the canonical home for `YOUR_TURN`,
206
+ * `PROMPT_OPENED` and `ACTION_REJECTED`. The plugin-side
207
+ * `<ToastProvider>` no longer mirrors these; consumers must pass
208
+ * the explicit `feedback` array sourced from the unified session
209
+ * store.
210
+ */
211
+ export function HostFeedbackToaster({
212
+ feedback = [],
213
+ onDismiss,
214
+ }: HostFeedbackToasterProps) {
215
+ const theme = useTheme();
216
+
217
+ useEffect(() => {
218
+ for (const item of feedback) {
219
+ const presentation = describeFeedback(item);
220
+ const dismiss = () => onDismiss?.(item.id);
221
+
222
+ // `toast.custom(jsx, { id })` is idempotent — calling it again
223
+ // with the same id updates the existing toast in place rather
224
+ // than mounting a duplicate, which is exactly what we want for
225
+ // host-feedback ids that may flow through several store
226
+ // snapshots before being dismissed.
227
+ toast.custom(
228
+ (toastId) => (
229
+ <HostFeedbackToastBody
230
+ presentation={presentation}
231
+ theme={theme}
232
+ onDismiss={() => {
233
+ toast.dismiss(toastId);
234
+ dismiss();
235
+ }}
236
+ />
237
+ ),
238
+ {
239
+ id: item.id,
240
+ duration: presentation.duration,
241
+ unstyled: true,
242
+ onAutoClose: () => dismiss(),
243
+ onDismiss: () => dismiss(),
244
+ },
245
+ );
246
+ }
247
+ }, [feedback, onDismiss, theme]);
248
+
249
+ return (
250
+ <Toaster
251
+ position="top-center"
252
+ // Disable `richColors` because we render fully themed bodies
253
+ // via `toast.custom` and don't want sonner's default success /
254
+ // error palette competing with the active theme.
255
+ richColors={false}
256
+ // Forward the OS-level theme hint so sonner's container
257
+ // (backdrop, default text colour for any non-custom toasts)
258
+ // matches the resolved Dreamboard theme mode.
259
+ theme={theme.meta.mode === "dark" ? "dark" : "light"}
260
+ toastOptions={{
261
+ // We use unstyled toasts so the inner `HostFeedbackToastBody`
262
+ // has full control. `unstyled: true` removes sonner's
263
+ // built-in surface, padding and shadow.
264
+ unstyled: true,
265
+ // Make the offset match sonner's default but anchor to the
266
+ // viewport with safe-area insets so the toast clears phone
267
+ // notches.
268
+ style: {
269
+ marginTop: "env(safe-area-inset-top, 0px)",
270
+ },
271
+ }}
272
+ />
273
+ );
274
+ }