@k2b/cloud 0.38.0 → 0.40.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 (244) hide show
  1. package/README.md +8 -0
  2. package/package.json +12 -5
  3. package/scripts/build-canvas-workers.ts +109 -29
  4. package/scripts/build.ts +3 -5
  5. package/src/_internal/app-favicon.ts +4 -2
  6. package/src/_internal/app-path.ts +25 -0
  7. package/src/_internal/app-presentation.ts +4 -0
  8. package/src/_internal/canvas-worker.ts +5 -3
  9. package/src/_internal/capabilities.ts +72 -48
  10. package/src/_internal/capability-reviews.ts +81 -0
  11. package/src/_internal/capability-schemas.ts +29 -0
  12. package/src/_internal/capability-sentences.ts +216 -1
  13. package/src/_internal/capability-streams.ts +7 -0
  14. package/src/_internal/define-app.ts +63 -16
  15. package/src/_internal/font-preloads.ts +2 -1
  16. package/src/_internal/panel-cookie.ts +39 -0
  17. package/src/_internal/registry-validation.ts +21 -26
  18. package/src/_internal/runtime-context.ts +1 -0
  19. package/src/_internal/runtime-watcher.ts +6 -0
  20. package/src/_internal/static-assets.ts +52 -3
  21. package/src/_internal/status-preserving-ssr.ts +2 -0
  22. package/src/access/PermissionEditor.browser-harness.tsx +57 -38
  23. package/src/access/PermissionEditor.tsx +32 -7
  24. package/src/access/ResourceApiKeys.tsx +5 -1
  25. package/src/account/EntitySearch.tsx +1 -1
  26. package/src/ai/action-review.ts +101 -0
  27. package/src/ai/approval-routes.ts +5 -2
  28. package/src/ai/approvals.ts +139 -28
  29. package/src/ai/background-review.ts +72 -0
  30. package/src/ai/browser.ts +10 -0
  31. package/src/ai/capabilities.ts +29 -11
  32. package/src/ai/capability-execution.ts +76 -43
  33. package/src/ai/capability-result.ts +95 -0
  34. package/src/ai/chat/action-card.ts +398 -0
  35. package/src/ai/chat/action-messages.ts +129 -0
  36. package/src/ai/chat/action-question.browser-harness.tsx +245 -0
  37. package/src/ai/chat/action-question.fixtures.ts +733 -0
  38. package/src/ai/chat/action-question.tsx +669 -0
  39. package/src/ai/chat/action-sentences.ts +1 -44
  40. package/src/ai/chat/blocks.tsx +19 -25
  41. package/src/ai/chat/message-actions.tsx +6 -0
  42. package/src/ai/chat/tool-action.tsx +481 -0
  43. package/src/ai/chat/turn-view.tsx +45 -163
  44. package/src/ai/chat/view-block.tsx +22 -7
  45. package/src/ai/chat-task-contracts.ts +31 -3
  46. package/src/ai/chat-task-reasons-migrate.ts +75 -0
  47. package/src/ai/chat-task-reasons.ts +92 -0
  48. package/src/ai/chat-tasks.ts +438 -78
  49. package/src/ai/client/controller.ts +59 -0
  50. package/src/ai/default-tools.ts +9 -1
  51. package/src/ai/executor.ts +190 -39
  52. package/src/ai/index.ts +14 -0
  53. package/src/ai/migrate.ts +39 -4
  54. package/src/ai/projects-routes.ts +3 -0
  55. package/src/ai/protocol.ts +14 -2
  56. package/src/ai/recent-resources.ts +240 -0
  57. package/src/ai/recent-sent-refs.ts +38 -0
  58. package/src/ai/request-context.ts +7 -1
  59. package/src/ai/routes.ts +172 -2
  60. package/src/ai/runtime.ts +13 -27
  61. package/src/ai/skills-routes.ts +3 -0
  62. package/src/ai/skills.ts +10 -1
  63. package/src/ai/store.ts +232 -27
  64. package/src/ai/timeline.ts +9 -1
  65. package/src/ai/tool-audit.ts +8 -0
  66. package/src/ai/tool-wording.ts +67 -0
  67. package/src/ai/turn-abort.ts +33 -0
  68. package/src/ai/types.ts +40 -4
  69. package/src/api/accounts-entities.ts +2 -1
  70. package/src/api/admin-ai-projects.ts +2 -0
  71. package/src/api/admin-ai-skills.ts +2 -0
  72. package/src/api/admin-ai-usage.ts +2 -0
  73. package/src/api/admin-core-settings.ts +190 -139
  74. package/src/api/admin-geoip.ts +80 -0
  75. package/src/api/admin-identity.ts +3 -0
  76. package/src/api/admin-lifecycle.ts +3 -0
  77. package/src/api/admin-outgoing-mail.ts +6 -2
  78. package/src/api/app-approval.ts +54 -6
  79. package/src/api/approvals-loader.ts +23 -1
  80. package/src/api/approvals.ts +3 -1
  81. package/src/api/auth.ts +25 -6
  82. package/src/api/capabilities.ts +44 -11
  83. package/src/api/capability-streams.ts +12 -0
  84. package/src/api/help.ts +4 -28
  85. package/src/api/index.ts +7 -1
  86. package/src/api/lasting-access-messages.ts +17 -0
  87. package/src/api/mcp.ts +3 -0
  88. package/src/api/me-panel-consent.ts +71 -0
  89. package/src/api/me.ts +80 -33
  90. package/src/api/search.ts +3 -1
  91. package/src/api/sync-ops.ts +2 -0
  92. package/src/api/widgets.ts +3 -0
  93. package/src/approvals/Approvals.tsx +180 -238
  94. package/src/approvals/messages.ts +12 -24
  95. package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
  96. package/src/browser/CloudResourceSearch.tsx +10 -6
  97. package/src/browser/app-approval.ts +60 -6
  98. package/src/browser/commands.ts +22 -6
  99. package/src/browser/composer-catalog.ts +145 -0
  100. package/src/browser/composer-insert-messages.ts +31 -0
  101. package/src/browser/composer-insert.ts +160 -0
  102. package/src/browser/composer-open-messages.ts +17 -0
  103. package/src/browser/composer-open.ts +110 -0
  104. package/src/browser/discoverable-command.ts +6 -0
  105. package/src/browser/hints.ts +6 -0
  106. package/src/browser/live-connection.ts +26 -2
  107. package/src/browser/navigation-apps.ts +7 -0
  108. package/src/browser/panel-context-bridge.ts +30 -0
  109. package/src/browser/panel-context-receiver.ts +53 -0
  110. package/src/browser/panel-menu.tsx +56 -0
  111. package/src/browser/panel-messages.ts +35 -0
  112. package/src/browser/panel-shortcuts.ts +31 -0
  113. package/src/browser/panel-slot.ts +756 -0
  114. package/src/browser/panel.ts +65 -0
  115. package/src/browser/resource-picker.tsx +12 -1
  116. package/src/browser/search-commands.ts +6 -3
  117. package/src/capabilities/client.ts +8 -2
  118. package/src/capabilities/reviews.ts +205 -0
  119. package/src/capabilities/server.ts +10 -3
  120. package/src/capabilities/testing.ts +47 -0
  121. package/src/capabilities/types.ts +2 -2
  122. package/src/cli/account.ts +13 -3
  123. package/src/cli/admin/geoip.ts +115 -0
  124. package/src/cli/admin/index.ts +3 -0
  125. package/src/contracts/app-approval.ts +159 -12
  126. package/src/contracts/app.ts +14 -0
  127. package/src/contracts/approvals.ts +2 -2
  128. package/src/contracts/capabilities.ts +410 -18
  129. package/src/contracts/geoip.ts +42 -0
  130. package/src/contracts/index.ts +3 -0
  131. package/src/contracts/panel-context-plain.ts +99 -0
  132. package/src/contracts/panel-context.ts +93 -0
  133. package/src/contracts/registry.ts +3 -1
  134. package/src/contracts/seen-hints.ts +35 -0
  135. package/src/contracts/shared.ts +2 -0
  136. package/src/server/client-ip.ts +5 -0
  137. package/src/server/help-payload.ts +38 -0
  138. package/src/server/middleware/auth.ts +11 -1
  139. package/src/server/middleware/middleware.ts +9 -0
  140. package/src/server/middleware/no-transform.ts +19 -0
  141. package/src/server/middleware/rate-limit.ts +4 -6
  142. package/src/server/request-context/address.ts +45 -0
  143. package/src/server/request-context/geoip.ts +380 -0
  144. package/src/server/request-context/index.ts +33 -0
  145. package/src/server/request-context/user-agent.ts +95 -0
  146. package/src/server/services/access.ts +5 -1
  147. package/src/services/accounts/posix.ts +2 -1
  148. package/src/services/app-approval.ts +533 -90
  149. package/src/services/approvals/device.ts +9 -5
  150. package/src/services/approvals/index.ts +6 -5
  151. package/src/services/approvals/list.ts +26 -5
  152. package/src/services/auth-flows/notification-sender.ts +3 -0
  153. package/src/services/geoip/index.ts +164 -0
  154. package/src/services/geoip/store.ts +30 -0
  155. package/src/services/geoip/upload.ts +150 -0
  156. package/src/services/identity/routes.ts +5 -1
  157. package/src/services/index.ts +32 -3
  158. package/src/services/known-browsers.ts +312 -0
  159. package/src/services/login-address-store.ts +51 -0
  160. package/src/services/login-context.ts +118 -0
  161. package/src/services/mandates/index.ts +3 -0
  162. package/src/services/mandates/policy.ts +15 -3
  163. package/src/services/notifications/browser.ts +16 -5
  164. package/src/services/notifications/email-frame.ts +1 -1
  165. package/src/services/oauth-tokens.ts +16 -2
  166. package/src/services/panel-context-consent.ts +58 -0
  167. package/src/services/pdf/image-protocol.ts +135 -0
  168. package/src/services/pdf/image-variants.ts +124 -0
  169. package/src/services/pdf/image-worker.ts +31 -0
  170. package/src/services/pdf/image.ts +109 -0
  171. package/src/services/pdf/index.ts +9 -0
  172. package/src/services/service-account-credentials.ts +21 -0
  173. package/src/services/session/index.ts +97 -16
  174. package/src/services/session/recent.ts +13 -0
  175. package/src/services/settings/api.ts +11 -6
  176. package/src/services/settings/app.ts +5 -4
  177. package/src/services/settings/core-settings.ts +4 -3
  178. package/src/services/settings/index.ts +19 -17
  179. package/src/services/settings/namespace.ts +2 -1
  180. package/src/services/settings/store.ts +83 -11
  181. package/src/services/webauthn.ts +12 -0
  182. package/src/shared/ai-platform-prompt.ts +1 -1
  183. package/src/shared/app-presentation.ts +1 -0
  184. package/src/shared/email-html.ts +2 -0
  185. package/src/shared/help.ts +3 -0
  186. package/src/shared/redirect.ts +12 -0
  187. package/src/shared-text/cursor.ts +22 -0
  188. package/src/shared-text/documents.ts +142 -0
  189. package/src/shared-text/events.ts +43 -0
  190. package/src/shared-text/index.ts +12 -0
  191. package/src/shared-text/log.ts +48 -0
  192. package/src/shared-text/text-diff.ts +23 -0
  193. package/src/shared-text/types.ts +41 -0
  194. package/src/ssr/AdminLayout.tsx +2 -0
  195. package/src/ssr/AppLaunchpadPanel.tsx +2 -2
  196. package/src/ssr/CloudShell.island.tsx +95 -10
  197. package/src/ssr/Layout.tsx +66 -10
  198. package/src/ssr/LayoutHeader.tsx +15 -9
  199. package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
  200. package/src/ssr/LayoutHelp.tsx +435 -274
  201. package/src/ssr/LayoutHelpPanel.browser-harness.tsx +136 -0
  202. package/src/ssr/LayoutHelpTrigger.tsx +27 -2
  203. package/src/ssr/LayoutRail.tsx +2 -2
  204. package/src/ssr/LayoutTabBar.tsx +29 -0
  205. package/src/ssr/MobileNavigation.tsx +2 -2
  206. package/src/ssr/PanelLayout.tsx +35 -0
  207. package/src/ssr/PanelRuntime.island.tsx +94 -0
  208. package/src/ssr/PanelSlot.tsx +158 -0
  209. package/src/ssr/PanelTrigger.tsx +49 -0
  210. package/src/ssr/ShellRuntime.island.tsx +69 -5
  211. package/src/ssr/app-appearance.ts +3 -2
  212. package/src/ssr/app-color.ts +105 -0
  213. package/src/ssr/app-launchpad.tsx +18 -1
  214. package/src/ssr/app-navigation.ts +28 -0
  215. package/src/ssr/compact-shell.baseline.json +5270 -0
  216. package/src/ssr/compact-shell.browser-harness.ts +439 -0
  217. package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
  218. package/src/ssr/help-messages.ts +4 -0
  219. package/src/ssr/help.ts +20 -0
  220. package/src/ssr/hints.ts +25 -0
  221. package/src/ssr/index.ts +5 -0
  222. package/src/ssr/islands/index.ts +4 -3
  223. package/src/ssr/layout-context.ts +2 -0
  224. package/src/ssr/layout-help-article.server.ts +50 -0
  225. package/src/ssr/layout-help-start.ts +33 -7
  226. package/src/ssr/panel-consent.ts +29 -0
  227. package/src/ssr/panel-context.ts +10 -0
  228. package/src/ssr/panel-slot.browser-harness.tsx +159 -0
  229. package/src/ssr/platform-messages.ts +48 -0
  230. package/src/ssr/rail-navigation.ts +44 -0
  231. package/src/ssr/shell-data.tsx +5 -0
  232. package/src/ssr/shell-shortcuts.ts +13 -2
  233. package/src/styles/effects.css +201 -11
  234. package/src/styles/file-chooser.css +2 -1
  235. package/src/styles/global.css +314 -63
  236. package/src/styles/panel.css +416 -0
  237. package/src/styles/resource-search.css +6 -2
  238. package/src/styles/tokens.css +36 -13
  239. package/src/styles/utilities-buttons.css +3 -2
  240. package/src/styles/utilities-data.css +2 -1
  241. package/src/styles/utilities-feedback.css +2 -2
  242. package/src/styles/utilities-layout.css +3 -13
  243. package/src/styles/utilities-markdown-editor.css +6 -6
  244. package/src/styles/utilities-navigation.css +22 -43
@@ -0,0 +1,6 @@
1
+ import { mergeSeenHints, parseSeenHintsCookieHeader, seenHintsSetCookie } from "../contracts/seen-hints";
2
+
3
+ /** Persist dismissal before navigating or closing the hint. */
4
+ export const markHintSeen = (id: string): void => {
5
+ document.cookie = seenHintsSetCookie(mergeSeenHints(parseSeenHintsCookieHeader(document.cookie), id), location.protocol === "https:");
6
+ };
@@ -10,6 +10,8 @@ export type LiveSubscriptionHandlers<T> = {
10
10
  apply: (events: { data: T; cursor: string }[]) => Promise<void>;
11
11
  /** Loads the canonical state again; events after it follow. */
12
12
  resync: () => Promise<void>;
13
+ /** Reports changes in whether the subscription is confirmed and has no pending work. Never called after `close()`. */
14
+ caughtUp?: (caughtUp: boolean) => void;
13
15
  /** The subscription ended because its resource is gone or no longer readable. */
14
16
  revoked?: (code: "not_found" | "access_denied") => void;
15
17
  /** Live updates stopped: the session ended, or `apply` or `resync` kept failing. Never called after `close()`. */
@@ -25,6 +27,7 @@ export type LiveConnection = {
25
27
  type Subscriber = {
26
28
  frame: () => unknown;
27
29
  confirmed: boolean;
30
+ report: () => void;
28
31
  receive: (message: LiveServerMessage) => void;
29
32
  end: () => void;
30
33
  };
@@ -56,9 +59,17 @@ const connect = (url: string, activity: "visible" | "always"): Shared => {
56
59
  onOpen: (controls) => {
57
60
  for (const subscriber of subscribers.values()) {
58
61
  subscriber.confirmed = false;
62
+ subscriber.report();
59
63
  controls.send(subscriber.frame());
60
64
  }
61
65
  },
66
+ onStatus: (status) => {
67
+ if (status === "open") return;
68
+ for (const subscriber of subscribers.values()) {
69
+ subscriber.confirmed = false;
70
+ subscriber.report();
71
+ }
72
+ },
62
73
  onMessage: (message) => {
63
74
  if (message.t === "progress") {
64
75
  for (const subscriber of subscribers.values()) if (subscriber.confirmed) subscriber.receive(message);
@@ -96,8 +107,16 @@ export const liveConnection = (url: string, options: { activity?: "visible" | "a
96
107
  let resyncAt: string | null = null;
97
108
  let ended = false;
98
109
  let running = false;
110
+ let caughtUp = false;
99
111
  const work: Work<T>[] = [];
100
112
 
113
+ const report = () => {
114
+ const next = !ended && subscriber.confirmed && !running && work.length === 0;
115
+ if (next === caughtUp) return;
116
+ caughtUp = next;
117
+ handlers.caughtUp?.(next);
118
+ };
119
+
101
120
  const attempt = async (run: () => Promise<void>): Promise<boolean> => {
102
121
  for (let tries = 0; ; tries++) {
103
122
  try {
@@ -122,6 +141,7 @@ export const liveConnection = (url: string, options: { activity?: "visible" | "a
122
141
  cursor = item.cursor;
123
142
  continue;
124
143
  }
144
+ report();
125
145
  const applied = item.kind === "events" ? await attempt(() => handlers.apply(item.events)) : await attempt(handlers.resync);
126
146
  if (!applied) {
127
147
  // A closed subscription reports nothing: its owner ended it.
@@ -135,6 +155,7 @@ export const liveConnection = (url: string, options: { activity?: "visible" | "a
135
155
  }
136
156
  } finally {
137
157
  running = false;
158
+ report();
138
159
  }
139
160
  };
140
161
 
@@ -153,6 +174,7 @@ export const liveConnection = (url: string, options: { activity?: "visible" | "a
153
174
  return { t: "sub", id, channel, scope, ...(after ? { after } : {}) };
154
175
  },
155
176
  confirmed: false,
177
+ report,
156
178
  receive: (message) => {
157
179
  if (ended) return;
158
180
  if (message.t === "revoked") {
@@ -189,10 +211,12 @@ export const liveConnection = (url: string, options: { activity?: "visible" | "a
189
211
  },
190
212
  };
191
213
 
192
- const stop = () => {
214
+ const stop = (notify = true) => {
193
215
  if (ended) return;
194
216
  ended = true;
195
217
  work.length = 0;
218
+ if (notify) report();
219
+ else caughtUp = false;
196
220
  if (subscribers.get(id) !== subscriber) return;
197
221
  subscribers.delete(id);
198
222
  socket.send({ t: "unsub", id });
@@ -205,6 +229,6 @@ export const liveConnection = (url: string, options: { activity?: "visible" | "a
205
229
  subscribers.set(id, subscriber);
206
230
  if (subscribers.size === 1) socket.connect();
207
231
  else socket.send(subscriber.frame());
208
- return { close: stop };
232
+ return { close: () => stop(false) };
209
233
  },
210
234
  });
@@ -0,0 +1,7 @@
1
+ import { readShellData } from "../ssr/shell-data";
2
+
3
+ /** Navigation controls discoverability, never authorization; the target application still checks access. */
4
+ export const navigationApps = (): { appIds: ReadonlySet<string>; currentAppId?: string } => {
5
+ const shell = readShellData();
6
+ return { appIds: new Set(shell?.apps.map((app) => app.id) ?? []), currentAppId: shell?.appId };
7
+ };
@@ -0,0 +1,30 @@
1
+ import type { CloudResourceRef } from "../contracts/capabilities";
2
+ import type { PanelPageContext } from "../contracts/panel-context";
3
+
4
+ export const PAGE_CONTEXT_COLLECT = "cloud.page-context.collect";
5
+ export const PAGE_CONTEXT_CHANGED = "cloud.page-context.changed";
6
+ export const PANEL_CONTEXT_CHANGED = "cloud.panel.context.changed";
7
+ export const PANEL_CONTEXT_ELEMENT = "cloud-panel-context";
8
+ export const PANEL_SHORTCUTS_ELEMENT = "cloud-panel-shortcuts";
9
+ export const PANEL_SHORTCUTS_CHANGED = "cloud.panel.shortcuts.changed";
10
+ export type PageContextSource = { scope?: CloudResourceRef };
11
+ export type PageContextCollection = { sources: PageContextSource[] };
12
+ export type PanelConsentRequest = { provider: string; allowed: boolean };
13
+ export type PanelState = { provider: string; appName: string; consent: boolean; allowed: boolean; saving: boolean; open: boolean };
14
+
15
+ declare global {
16
+ interface WindowEventMap {
17
+ "cloud.panel.consent": CustomEvent<PanelConsentRequest>;
18
+ "cloud.panel.state": CustomEvent<PanelState>;
19
+ }
20
+ }
21
+
22
+ /** Framework-validated SSR data, then receiver-validated messages. Shared DOM state survives independent bundles. */
23
+ export function readProviderContext(): PanelPageContext | null {
24
+ if (typeof document === "undefined") return null;
25
+ try {
26
+ return JSON.parse(document.getElementById(PANEL_CONTEXT_ELEMENT)?.textContent ?? "null");
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
@@ -0,0 +1,53 @@
1
+ import { isPanelContextMessage } from "../contracts/panel-context-plain";
2
+ import {
3
+ PANEL_CONTEXT_CHANGED,
4
+ PANEL_CONTEXT_ELEMENT,
5
+ PANEL_SHORTCUTS_CHANGED,
6
+ PANEL_SHORTCUTS_ELEMENT,
7
+ readProviderContext,
8
+ } from "./panel-context-bridge";
9
+
10
+ /** Installed only by an embedded PanelRuntime; the public page publisher never imports validation. */
11
+ export function installPanelContextReceiver(): () => void {
12
+ let element = document.getElementById(PANEL_CONTEXT_ELEMENT);
13
+ if (!element) {
14
+ element = document.createElement("script");
15
+ element.id = PANEL_CONTEXT_ELEMENT;
16
+ element.setAttribute("type", "application/json");
17
+ element.textContent = "null";
18
+ document.body.append(element);
19
+ }
20
+ let shortcutsElement = document.getElementById(PANEL_SHORTCUTS_ELEMENT);
21
+ if (!shortcutsElement) {
22
+ shortcutsElement = document.createElement("script");
23
+ shortcutsElement.id = PANEL_SHORTCUTS_ELEMENT;
24
+ shortcutsElement.setAttribute("type", "application/json");
25
+ shortcutsElement.textContent = "[]";
26
+ document.body.append(shortcutsElement);
27
+ }
28
+ const first = readProviderContext();
29
+ let pageId = first?.pageId;
30
+ let seq = first?.seq ?? 0;
31
+ const receive = (event: MessageEvent<unknown>) => {
32
+ if (event.source !== window.parent || event.origin !== location.origin || !isPanelContextMessage(event.data)) return;
33
+ const context = event.data.context;
34
+ if (context) {
35
+ if ((pageId !== undefined && context.pageId !== pageId) || context.seq < seq) return;
36
+ pageId = context.pageId;
37
+ seq = context.seq;
38
+ }
39
+ // Null revokes the current context without forgetting document identity or ordering.
40
+ const contextJson = JSON.stringify(context);
41
+ const contextChanged = element.textContent !== contextJson;
42
+ element.textContent = contextJson;
43
+ const shortcuts = JSON.stringify(context ? (event.data.shortcuts ?? []) : []);
44
+ const shortcutsChanged = shortcutsElement.textContent !== shortcuts;
45
+ shortcutsElement.textContent = shortcuts;
46
+ // Remember accepted snapshots, including empty ones, across Help remounts.
47
+ shortcutsElement.setAttribute("data-received", "");
48
+ if (shortcutsChanged) window.dispatchEvent(new Event(PANEL_SHORTCUTS_CHANGED));
49
+ if (contextChanged || !shortcutsChanged) window.dispatchEvent(new Event(PANEL_CONTEXT_CHANGED));
50
+ };
51
+ window.addEventListener("message", receive);
52
+ return () => window.removeEventListener("message", receive);
53
+ }
@@ -0,0 +1,56 @@
1
+ import { Dropdown } from "@k2b/ui";
2
+ import { createSignal, onCleanup, onMount } from "solid-js";
3
+ import { platformMessages } from "../ssr/platform-messages";
4
+ import type { PanelState } from "./panel-context-bridge";
5
+
6
+ /**
7
+ * The slot head's ⋯ menu for providers that need consent. The lazy slot controller mounts it over the server's
8
+ * placeholder button, so it costs no island and can never change an answer before the controller listens. Only
9
+ * document events cross the menu/controller boundary.
10
+ */
11
+ export default function PanelMenu(props: { initial: PanelState; locale: string }) {
12
+ const t = () => platformMessages.resolve([props.locale]).t;
13
+ const [state, setState] = createSignal(props.initial);
14
+ const [open, setOpen] = createSignal(false);
15
+ onMount(() => {
16
+ const changed = (event: CustomEvent<PanelState>) => {
17
+ // A closed slot, another provider or one without consent leaves no reason for the menu to stay expanded.
18
+ if (!event.detail.open || event.detail.provider !== state().provider || !event.detail.consent) setOpen(false);
19
+ setState(event.detail);
20
+ };
21
+ window.addEventListener("cloud.panel.state", changed);
22
+ onCleanup(() => window.removeEventListener("cloud.panel.state", changed));
23
+ });
24
+ return (
25
+ <Dropdown.Root
26
+ open={open()}
27
+ onOpenChange={setOpen}
28
+ label={t().panelMenu}
29
+ position="bottom-left"
30
+ items={[
31
+ {
32
+ sectionLabel: t().panelFrom({ app: state().appName }),
33
+ items: [
34
+ {
35
+ label: t().panelShareContext,
36
+ choice: "checkbox",
37
+ // A getter: the menu keeps its rows, so a plain value would show the answer from when it opened.
38
+ checked: () => state().allowed,
39
+ disabled: state().saving,
40
+ action: () =>
41
+ window.dispatchEvent(
42
+ new CustomEvent("cloud.panel.consent", {
43
+ detail: { provider: state().provider, allowed: !state().allowed },
44
+ }),
45
+ ),
46
+ },
47
+ ],
48
+ },
49
+ ]}
50
+ >
51
+ <Dropdown.Trigger iconOnly label={t().panelMenu} disabled={!state().consent}>
52
+ <i class="ti ti-dots" aria-hidden="true" />
53
+ </Dropdown.Trigger>
54
+ </Dropdown.Root>
55
+ );
56
+ }
@@ -0,0 +1,35 @@
1
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
2
+
3
+ export function isPanelReadyMessage(value: unknown, provider: string): value is { type: "cloud.panel.ready"; v: 1; provider: string } {
4
+ return (
5
+ isRecord(value) &&
6
+ Reflect.ownKeys(value).length === 3 &&
7
+ Object.hasOwn(value, "type") &&
8
+ Object.hasOwn(value, "v") &&
9
+ Object.hasOwn(value, "provider") &&
10
+ value.type === "cloud.panel.ready" &&
11
+ value.v === 1 &&
12
+ typeof value.provider === "string" &&
13
+ value.provider === provider
14
+ );
15
+ }
16
+
17
+ export function isPanelSearchMessage(value: unknown): value is { type: "cloud.panel.search"; v: 1 } {
18
+ return (
19
+ isRecord(value) &&
20
+ Reflect.ownKeys(value).length === 2 &&
21
+ Object.hasOwn(value, "type") &&
22
+ Object.hasOwn(value, "v") &&
23
+ value.type === "cloud.panel.search" &&
24
+ value.v === 1
25
+ );
26
+ }
27
+
28
+ export function isPanelCloseMessage(value: unknown): value is { type: "cloud.panel.close"; v?: 1 } {
29
+ return (
30
+ isRecord(value) &&
31
+ Object.hasOwn(value, "type") &&
32
+ value.type === "cloud.panel.close" &&
33
+ (Reflect.ownKeys(value).length === 1 || (Reflect.ownKeys(value).length === 2 && Object.hasOwn(value, "v") && value.v === 1))
34
+ );
35
+ }
@@ -0,0 +1,31 @@
1
+ import { isPanelContextMessage } from "../contracts/panel-context-plain";
2
+ import type { HelpShortcut } from "../ssr/layout-help-start";
3
+ import { PANEL_SHORTCUTS_CHANGED, PANEL_SHORTCUTS_ELEMENT, readProviderContext } from "./panel-context-bridge";
4
+
5
+ /** Only platform Help code imports this module; third-party providers never receive a list. */
6
+ export function readPanelShortcuts(): HelpShortcut[] {
7
+ if (typeof document === "undefined") return [];
8
+ try {
9
+ const shortcuts: unknown = JSON.parse(document.getElementById(PANEL_SHORTCUTS_ELEMENT)?.textContent ?? "[]");
10
+ const message = {
11
+ type: "cloud.panel.context",
12
+ v: 1,
13
+ context: readProviderContext(),
14
+ shortcuts,
15
+ };
16
+ return isPanelContextMessage(message) ? (message.shortcuts ?? []) : [];
17
+ } catch {
18
+ return [];
19
+ }
20
+ }
21
+
22
+ export function panelShortcutsReceived(): boolean {
23
+ if (typeof document === "undefined") return false;
24
+ return document.getElementById(PANEL_SHORTCUTS_ELEMENT)?.hasAttribute("data-received") ?? false;
25
+ }
26
+
27
+ export function onPanelShortcutsChange(cb: (shortcuts: HelpShortcut[]) => void): () => void {
28
+ const changed = () => cb(readPanelShortcuts());
29
+ window.addEventListener(PANEL_SHORTCUTS_CHANGED, changed);
30
+ return () => window.removeEventListener(PANEL_SHORTCUTS_CHANGED, changed);
31
+ }