@k2b/cloud 0.36.0 → 0.38.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 (90) hide show
  1. package/package.json +5 -3
  2. package/scripts/README.md +1 -0
  3. package/scripts/cli-pack.ts +123 -0
  4. package/src/_internal/help-catalog.ts +123 -4
  5. package/src/_internal/help.ts +27 -6
  6. package/src/_internal/shell-focus.ts +11 -0
  7. package/src/access/PermissionEditor.browser-harness.tsx +46 -0
  8. package/src/access/PermissionEditor.tsx +38 -9
  9. package/src/access/ui.ts +1 -0
  10. package/src/account/Avatar.tsx +20 -2
  11. package/src/account/ui.ts +1 -1
  12. package/src/ai/browser.ts +1 -0
  13. package/src/ai/chart-block.ts +44 -13
  14. package/src/ai/chat/answer-links.ts +8 -0
  15. package/src/ai/chat/chart-block.tsx +2 -11
  16. package/src/ai/chat/messages.ts +12 -0
  17. package/src/ai/chat/primitives.tsx +2 -1
  18. package/src/ai/chat/web-tools.tsx +23 -6
  19. package/src/ai/chat-tasks.ts +65 -1
  20. package/src/ai/default-tool-contracts.ts +30 -13
  21. package/src/ai/executor.ts +46 -3
  22. package/src/ai/fetch-file-tool.ts +37 -3
  23. package/src/ai/migrate.ts +19 -2
  24. package/src/ai/readable-html.ts +1057 -0
  25. package/src/ai/types.ts +1 -0
  26. package/src/api/help.ts +23 -4
  27. package/src/api/mcp.ts +8 -2
  28. package/src/browser/FileChooser.tsx +11 -3
  29. package/src/browser/choose-files.browser-harness.tsx +4 -1
  30. package/src/browser/file-chooser-messages.ts +4 -0
  31. package/src/browser/file-providers.ts +14 -1
  32. package/src/browser/provider-folder.tsx +58 -5
  33. package/src/browser/resource-picker.tsx +5 -1
  34. package/src/browser/shell-focus.ts +67 -0
  35. package/src/cli/admin/workflows.ts +1 -0
  36. package/src/cli/commands.ts +2 -0
  37. package/src/cli/plugin.ts +5 -5
  38. package/src/config/env.ts +14 -0
  39. package/src/contracts/file-provider.ts +48 -1
  40. package/src/contracts/outgoing-mail.ts +8 -1
  41. package/src/server/help.ts +136 -10
  42. package/src/server/services/geo.ts +21 -4
  43. package/src/services/allowed-network-fetch.ts +189 -0
  44. package/src/services/help/index.ts +46 -5
  45. package/src/services/help/store.ts +26 -5
  46. package/src/services/help/types.ts +10 -2
  47. package/src/services/index.ts +2 -0
  48. package/src/services/network-security.ts +28 -0
  49. package/src/services/outgoing-mail/dispatcher.ts +7 -2
  50. package/src/services/outgoing-mail/resolve-host.ts +39 -0
  51. package/src/services/outgoing-mail/test-send.ts +18 -3
  52. package/src/services/outgoing-mail/transport.ts +24 -13
  53. package/src/services/settings/index.ts +48 -26
  54. package/src/shared/help.ts +41 -0
  55. package/src/shared/index.ts +9 -1
  56. package/src/shared/markdown/extensions/guided-help.ts +208 -64
  57. package/src/shared/markdown/formula.ts +8 -8
  58. package/src/shared/markdown/index.ts +72 -11
  59. package/src/shared/network-address.ts +9 -0
  60. package/src/ssr/CloudShell.island.tsx +2 -0
  61. package/src/ssr/GlobalSearchTrigger.tsx +1 -1
  62. package/src/ssr/Layout.tsx +28 -2
  63. package/src/ssr/LayoutHelp.browser-harness.tsx +130 -27
  64. package/src/ssr/LayoutHelp.tsx +569 -410
  65. package/src/ssr/LayoutHelpTrigger.tsx +1 -1
  66. package/src/ssr/LayoutRail.tsx +1 -1
  67. package/src/ssr/ProfilePreferences.tsx +1 -1
  68. package/src/ssr/RailApps.tsx +1 -1
  69. package/src/ssr/ShellFocusToggle.tsx +43 -0
  70. package/src/ssr/ShellRuntime.island.tsx +53 -26
  71. package/src/ssr/app-launchpad.tsx +1 -1
  72. package/src/ssr/help-messages.ts +60 -38
  73. package/src/ssr/help.ts +3 -0
  74. package/src/ssr/layout-help-article.ts +41 -0
  75. package/src/ssr/layout-help-navigation.ts +0 -12
  76. package/src/ssr/layout-help-registry.tsx +4 -11
  77. package/src/ssr/layout-help-search.ts +9 -12
  78. package/src/ssr/layout-help-start.ts +68 -0
  79. package/src/ssr/platform-messages.ts +14 -0
  80. package/src/ssr/shell-data.tsx +7 -0
  81. package/src/ssr/shell-focus.browser-harness.tsx +83 -0
  82. package/src/ssr/shell-shortcuts.ts +8 -0
  83. package/src/styles/effects.css +688 -185
  84. package/src/styles/global.css +155 -26
  85. package/src/styles/resource-search.css +25 -0
  86. package/src/styles/utilities-navigation.css +11 -3
  87. package/src/workflows/store/events.ts +24 -0
  88. package/src/workflows/store/index.ts +8 -0
  89. package/src/workflows/store/observability.ts +6 -1
  90. package/src/workflows/store/retention.ts +157 -0
@@ -1,7 +1,8 @@
1
1
  import type { DateContext } from "@k2b/stdlib";
2
- import { AppWorkspace, appWorkspaceLayoutStyle, LocaleProvider, NoticeCard, PageTrailProvider } from "@k2b/ui";
2
+ import { AppWorkspace, appWorkspaceLayoutStyle, keyboardPlatform, LocaleProvider, NoticeCard, PageTrailProvider } from "@k2b/ui";
3
3
  import type { JSX } from "solid-js/jsx-runtime";
4
4
  import { readAppWorkspaceLayoutCookie, resolveAppWorkspaceLayoutForSidebar } from "../_internal/app-workspace-state";
5
+ import { readShellFocusCookie } from "../_internal/shell-focus";
5
6
  import { resolveNavMatch } from "../contracts/app"; // ==========================
6
7
  import { isPwaShellAvailable } from "../contracts/pwa";
7
8
  import { defaultRailPreferences } from "../contracts/rail-preferences";
@@ -13,11 +14,12 @@ import { readThemePreferenceFromCookieHeader } from "../shared/theme";
13
14
  import AnnouncementBanners from "./AnnouncementBanners";
14
15
  import { appAppearanceStyle, resolveCurrentApp } from "./app-appearance";
15
16
  import { resolveRuntimeRoute, visibleNavigationApps } from "./app-navigation";
16
- import { getLayoutHelp } from "./help";
17
+ import { getLayoutHelp, layoutHelpFailed } from "./help";
17
18
  import LayoutFooter from "./LayoutFooter";
18
19
  import LayoutHeader from "./LayoutHeader";
19
20
  import LayoutRail, { type LayoutAppLink } from "./LayoutRail";
20
21
  import type { LayoutContext } from "./layout-context";
22
+ import { HELP_VIEW_LIMIT } from "./layout-help-start";
21
23
  import type { LayoutBreadcrumb } from "./layout-runtime";
22
24
  import PageTrailRow from "./PageTrailRow";
23
25
  import { derivePageTrail } from "./page-trail";
@@ -25,6 +27,7 @@ import { platformMessages } from "./platform-messages";
25
27
  import { sortRailApps } from "./rail-navigation";
26
28
  import { requestPath } from "./request-path";
27
29
  import { getLocalizedRuntimeContext, type RuntimeContext } from "./runtime";
30
+ import ShellFocusToggle from "./ShellFocusToggle";
28
31
  import ShellRuntime from "./ShellRuntime.island";
29
32
  import { type ShellData, ShellDataProvider } from "./shell-data";
30
33
 
@@ -34,10 +37,14 @@ type LayoutProps = {
34
37
  children: JSX.Element;
35
38
  c: LayoutContext;
36
39
  title?: string | Breadcrumb[];
40
+ /** Up to three ids of the current app's Help articles that fit this page; Help lists them first under "For this view". Unknown ids and articles the reader may not see are ignored. */
41
+ help?: readonly string[];
37
42
  fullPage?: boolean /** Keep the shell viewport-bound and suppress its footer. */;
38
43
  fullWidth?: boolean /** Delegate scrolling and clipping to the page's work surface. */;
39
44
  focusMode?: boolean /** Render only the app canvas and content for dedicated editors or pop-out windows. */;
40
45
  flushCanvas?: boolean /** Remove app-canvas spacing for edge-to-edge focus surfaces such as pop-out windows. */;
46
+ /** Final localized app-specific export or printing guidance, shown when no data-print-region exists. */
47
+ printHint?: string;
41
48
  workspaceSidebarCollapsible?: boolean /** Resolve persisted sidebar geometry for the page's sidebar policy during SSR. */;
42
49
  }; // ==========================
43
50
  // Helpers
@@ -187,6 +194,8 @@ export default function Layout(props: LayoutProps) {
187
194
  ? { title: pageTitle, ancestors: [] }
188
195
  : derivePageTrail(title ?? appName, currentRoute?.prefix, appName, `${pathname}${new URL(c.req.raw.url).search}`);
189
196
  const showRail = !!user;
197
+ // The app bar and the app's navigation hidden with Cmd/Ctrl+\ or the sidebar buttons; phones ignore it in CSS.
198
+ const shellFocus = showRail && readShellFocusCookie(cookie);
190
199
  // The profile menu offers the mobile app only while the installation runs it.
191
200
  const appHref = user && isPwaShellAvailable(runtime.apps) ? "/me/app" : undefined;
192
201
  const profileName = user?.displayName || user?.uid || "?";
@@ -202,7 +211,12 @@ export default function Layout(props: LayoutProps) {
202
211
  .filter(Boolean)
203
212
  .join(";") || undefined;
204
213
  const appLabel = currentApp?.name ?? (pathname.startsWith("/admin") ? t.admin : undefined);
214
+ const helpIds = new Set(registeredHelp?.documents.map((topic) => topic.id));
215
+ const viewHelp = [...new Set(props.help)].filter((id) => helpIds.has(id)).slice(0, HELP_VIEW_LIMIT);
205
216
  const shellData: ShellData = {
217
+ platform: keyboardPlatform(c.req.raw.headers.get("User-Agent") ?? ""),
218
+ ...(viewHelp.length ? { help: viewHelp } : {}),
219
+ ...(layoutHelpFailed(c) ? { helpFailed: true as const } : {}),
206
220
  apps: allApps,
207
221
  ...(user
208
222
  ? { rail: c.get("railPreferences") ?? defaultRailPreferences(), profile: { name: profileName, avatarSrc: profileAvatarSrc, appHref } }
@@ -231,6 +245,10 @@ export default function Layout(props: LayoutProps) {
231
245
  <AppWorkspace.LayoutStateProvider state={workspaceLayout}>{props.children}</AppWorkspace.LayoutStateProvider>
232
246
  </main>
233
247
  </PageTrailProvider>
248
+ <div data-print-fallback="">
249
+ <p>{t.printFallbackTitle}</p>
250
+ <p>{props.printHint ?? t.printFallbackHint}</p>
251
+ </div>
234
252
  </div>
235
253
  </LocaleProvider>
236
254
  );
@@ -248,11 +266,13 @@ export default function Layout(props: LayoutProps) {
248
266
  data-page-trail-title-only={titleOnly ? "" : undefined}
249
267
  data-layout-full-page={fullPage ? "true" : undefined}
250
268
  data-layout-authenticated={showRail ? "true" : undefined}
269
+ data-shell-focus={shellFocus ? "true" : undefined}
251
270
  data-workspace-sidebar-collapsed={workspaceLayout?.sidebarCollapsed ? "true" : undefined}
252
271
  >
253
272
  <ShellRuntime
254
273
  workspace={{ appId: currentApp?.id }}
255
274
  pageTrail
275
+ focus={showRail}
256
276
  help={runtimeHelp}
257
277
  commands={{ searchResources: !!user }}
258
278
  push={!!user}
@@ -265,6 +285,8 @@ export default function Layout(props: LayoutProps) {
265
285
  />
266
286
  <ShellDataProvider data={shellData}>
267
287
  {showRail && <LayoutRail currentPath={requestPath(c)} />}
288
+ {showRail && <ShellFocusToggle variant="float" />}
289
+ {showRail && <div class="cloud-shell-reveal" data-cloud-shell-reveal="" aria-hidden="true" />}
268
290
  <div class="layout-shell-content flex min-h-0 min-w-0 flex-1 flex-col">
269
291
  <LayoutHeader
270
292
  authenticated={showRail}
@@ -284,6 +306,10 @@ export default function Layout(props: LayoutProps) {
284
306
  {!fullPage && !showRail && <LayoutFooter appName={settings?.app?.copyright || appName} legalLinks={legalLinks} />}
285
307
  </div>
286
308
  </ShellDataProvider>
309
+ <div data-print-fallback="">
310
+ <p>{t.printFallbackTitle}</p>
311
+ <p>{props.printHint ?? t.printFallbackHint}</p>
312
+ </div>
287
313
  </div>
288
314
  </LocaleProvider>
289
315
  );
@@ -1,43 +1,146 @@
1
- import { LocaleProvider } from "@k2b/ui";
1
+ import { AppWorkspace, LocaleProvider } from "@k2b/ui";
2
2
  import { render } from "solid-js/web";
3
+ import { registerContextAwareCommand } from "../browser/command-bridge";
3
4
  import type { HelpDocumentManifest, HelpDocumentPayload } from "../shared/help";
4
- import { LayoutHelpPage } from "./LayoutHelp";
5
+ import { LayoutHelpPage, openLayoutHelpDialog } from "./LayoutHelp";
6
+ import { registry } from "./layout-help-registry";
5
7
 
6
- const documents: HelpDocumentManifest[] = Array.from({ length: 8 }, (_, index) => ({
7
- id: `topic-${index}`,
8
- title: `Topic ${index}`,
8
+ const params = new URL(location.href).searchParams;
9
+ const fixture = (id: string, title: string, order: number, kind?: HelpDocumentManifest["kind"], icon?: string): HelpDocumentManifest => ({
10
+ id,
11
+ title,
9
12
  description: "Help browser fixture",
10
- order: index,
11
- url: `/api/help/topic-${index}`,
13
+ order,
14
+ ...(kind ? { kind } : {}),
15
+ ...(icon ? { icon } : {}),
16
+ url: `/api/help/${id}`,
12
17
  searchUrl: "/api/help/search",
13
- }));
14
- const html = Array.from(
15
- { length: 8 },
16
- (_, index) =>
17
- `<h2 id="section-${index}" class="help-section-title" data-help-icon="ti ti-point">Section ${index}</h2>${Array.from({ length: 18 }, () => "<p>Read this guidance to finish the task and understand how the app works.</p>").join("")}`,
18
- ).join("");
18
+ });
19
+ // `?start`: an app with every kind and more tasks than the start shows as tiles.
20
+ const startDocuments: HelpDocumentManifest[] = [
21
+ fixture("share-space", "Share a Space", 10, "task", "ti ti-share"),
22
+ fixture("claim-task", "Claim a task", 20, "task", "ti ti-hand-grab"),
23
+ fixture("statuses", "Create and order statuses", 30, "task", "ti ti-arrows-sort"),
24
+ fixture("calendar-feed", "See dates in your own calendar", 40, "task", "ti ti-calendar-share"),
25
+ fixture("templates", "Start from a template", 50, undefined, "ti ti-template"),
26
+ fixture("filters", "Filter and find items", 60, "task", "ti ti-filter"),
27
+ fixture("handoffs", "Hand work over", 70, "task", "ti ti-arrows-exchange"),
28
+ fixture("archive", "Archive a Space", 80, "task", "ti ti-archive"),
29
+ fixture("import", "Import tasks from a spreadsheet with a very long title that wraps", 90, "task", "ti ti-table-import"),
30
+ fixture("recurring", "Repeat a task", 100, "task", "ti ti-repeat"),
31
+ fixture("start", "Start with Spaces", 5, "start", "ti ti-rocket"),
32
+ fixture("access-levels", "Access levels", 110, "reference", "ti ti-key"),
33
+ fixture("troubleshoot", "Fix problems in Spaces", 120, "troubleshoot", "ti ti-lifebuoy"),
34
+ ];
35
+ const documents: HelpDocumentManifest[] = params.has("start")
36
+ ? startDocuments
37
+ : Array.from({ length: 8 }, (_, index) => fixture(`topic-${index}`, `Topic ${index}`, index));
38
+ // `?commands`: what the shell and an app register on a real page.
39
+ if (params.has("commands")) {
40
+ for (const command of [
41
+ { id: "cloud.shell.focus", title: "Hide sidebar", description: "", shortcut: "mod+\\" },
42
+ { id: "cloud.search", title: "Open global search", description: "", shortcut: "mod+k" },
43
+ { id: "cloud.help", title: "Open shortcut help", description: "", shortcut: "shift+/" },
44
+ { id: "spaces.new", title: "New task", description: "", shortcut: "n" },
45
+ { id: "spaces.filter", title: "Filter tasks", description: "", shortcut: "f" },
46
+ { id: "spaces.complete", title: "Complete selected", description: "", shortcut: "x", scope: "selection" as const },
47
+ ])
48
+ registerContextAwareCommand({ ...command, action: () => {} });
49
+ }
50
+ const lead = "<p>Share a Space so that other people can see and change its tasks.</p>";
51
+ const longHtml =
52
+ lead +
53
+ Array.from(
54
+ { length: 8 },
55
+ (_, index) =>
56
+ `<h2 id="section-${index}" class="help-section-title">Section ${index}</h2>${Array.from({ length: 18 }, () => "<p>Read this guidance to finish the task and understand how the app works.</p>").join("")}`,
57
+ ).join("");
58
+ // A short task article: the short answer, one section, four steps.
59
+ const taskHtml = `${lead}<h2 id="invite-people" class="help-section-title">Invite people</h2><div class="help-steps"><ol class="k2b-steps"><li>Open <span class="k2b-label-chip">Space settings</span>.</li><li>Choose <span class="k2b-label-chip">Add person</span>.</li><li>Enter a name or a group.</li><li>Choose the access level.</li></ol></div><p>Spaces saves the change at once.</p><div class="help-compare"><ul><li><strong>Add a person</strong> Gives one named person access to every task in this Space.</li><li><strong>Add a group</strong> Gives everyone in the group access, including people who join later.</li></ul></div>`;
60
+ const link = (id: string, title: string, appId = "spaces", appName = "Spaces") => ({
61
+ appId,
62
+ appName,
63
+ accent: appId === "spaces" ? "#16a34a" : "#2563eb",
64
+ documentId: id,
65
+ title,
66
+ description: "Help browser fixture",
67
+ url: `/help/apps/${appId}/${id}`,
68
+ });
19
69
  window.fetch = Object.assign(
20
70
  async (input: URL | RequestInfo) => {
21
71
  await new Promise((resolve) => setTimeout(resolve, 80));
22
72
  const path = String(input);
23
- if (path.includes("/search")) return Response.json({ locale: "en", ids: ["topic-0", "topic-1"] });
73
+ if (path.includes("/search")) {
74
+ const query = new URL(path, location.href).searchParams.get("q") ?? "";
75
+ const ids = params.has("start")
76
+ ? documents.filter((document) => document.title.toLocaleLowerCase().includes(query)).map((document) => document.id)
77
+ : ["topic-0", "topic-1"];
78
+ return Response.json({ locale: "en", ids });
79
+ }
24
80
  const id = path.split("/").at(-1) ?? "topic-0";
25
- const payload: HelpDocumentPayload = { id, locale: "en", title: id, markdown: "Help", html };
81
+ const payload: HelpDocumentPayload = {
82
+ id,
83
+ locale: "en",
84
+ title: id,
85
+ markdown: "Help",
86
+ html: id === "topic-1" ? taskHtml : longHtml,
87
+ kind: "task",
88
+ needs: "manage",
89
+ next: link("topic-2", "Topic 2"),
90
+ related: [link("topic-3", "Topic 3"), link("mail-share", "Share a mailbox", "mail", "Mail")],
91
+ };
26
92
  return Response.json(payload);
27
93
  },
28
94
  { preconnect: () => {} },
29
95
  );
30
96
  const main = document.querySelector<HTMLElement>(".layout-content-main");
31
97
  if (!main) throw new Error("Missing Layout main");
32
- const embedded = new URL(location.href).searchParams.has("embedded");
33
- const host = document.createElement("div");
34
- if (embedded) host.style.cssText = "width:360px;height:700px";
35
- main.append(host);
36
- render(
37
- () => (
38
- <LocaleProvider locale="en">
39
- <LayoutHelpPage documents={documents} embedded={embedded} pageBase="/app/spaces/help" />
40
- </LocaleProvider>
41
- ),
42
- host,
43
- );
98
+ const embedded = params.has("embedded");
99
+ const referenceHost = params.get("host");
100
+ if (referenceHost === "grids" || referenceHost === "pulse") {
101
+ // The Grids and Pulse query reference windows: one article beside the reference sidebar, without the Cloud shell.
102
+ const host = document.createElement("div");
103
+ host.className = "h-dvh";
104
+ document.body.replaceChildren(host);
105
+ const reader = () => <LayoutHelpPage documents={documents.slice(0, 1)} initialTopic="topic-0" includeShortcuts={false} embedded />;
106
+ render(
107
+ () => (
108
+ <LocaleProvider locale="en">
109
+ <AppWorkspace>
110
+ <AppWorkspace.Sidebar>
111
+ <AppWorkspace.SidebarDesktop>
112
+ <AppWorkspace.SidebarBody>Reference</AppWorkspace.SidebarBody>
113
+ </AppWorkspace.SidebarDesktop>
114
+ </AppWorkspace.Sidebar>
115
+ <AppWorkspace.Content>
116
+ {referenceHost === "grids" ? (
117
+ <AppWorkspace.Main class="p-[var(--ui-space-shell)]" scroll={false}>
118
+ <div class="flex min-h-0 flex-1 flex-col overflow-auto">{reader()}</div>
119
+ </AppWorkspace.Main>
120
+ ) : (
121
+ <AppWorkspace.Main class="overflow-y-auto">{reader()}</AppWorkspace.Main>
122
+ )}
123
+ </AppWorkspace.Content>
124
+ </AppWorkspace>
125
+ </LocaleProvider>
126
+ ),
127
+ host,
128
+ );
129
+ } else if (params.has("modal")) {
130
+ // The dialog reads the topics the page registered, as Layout.HelpDocuments does.
131
+ for (const manifest of documents) registry()?.set(manifest.id, manifest);
132
+ window.__cloudLayoutHelpPageBase = "/app/spaces/help";
133
+ openLayoutHelpDialog();
134
+ } else {
135
+ const host = document.createElement("div");
136
+ if (embedded) host.style.cssText = "width:360px;height:700px";
137
+ main.append(host);
138
+ render(
139
+ () => (
140
+ <LocaleProvider locale="en">
141
+ <LayoutHelpPage documents={documents} embedded={embedded} pageBase="/app/spaces/help" />
142
+ </LocaleProvider>
143
+ ),
144
+ host,
145
+ );
146
+ }