@k2b/cloud 0.32.0 → 0.33.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 (63) hide show
  1. package/package.json +5 -5
  2. package/src/_internal/define-app.ts +17 -8
  3. package/src/_internal/page-responses.ts +5 -1
  4. package/src/ai/code-source-contracts.ts +17 -2
  5. package/src/ai/code-source-tools.ts +1 -0
  6. package/src/ai/default-tools.ts +4 -4
  7. package/src/ai/memories.ts +48 -1
  8. package/src/ai/memory-learning.ts +17 -6
  9. package/src/ai/memory-tool.ts +1 -0
  10. package/src/ai/migrate.ts +8 -0
  11. package/src/ai/provider.ts +4 -3
  12. package/src/browser/navigation-search.ts +4 -1
  13. package/src/services/outbox.ts +13 -6
  14. package/src/shared/ai-platform-prompt.ts +3 -2
  15. package/src/shared/theme.ts +41 -20
  16. package/src/ssr/AnnouncementBanners.tsx +30 -0
  17. package/src/ssr/AppLaunchpadPanel.tsx +2 -2
  18. package/src/ssr/CloudShell.island.tsx +91 -0
  19. package/src/ssr/GlobalSearchTrigger.tsx +51 -0
  20. package/src/ssr/Layout.tsx +56 -85
  21. package/src/ssr/LayoutHeader.tsx +31 -77
  22. package/src/ssr/LayoutHelp.tsx +2 -57
  23. package/src/ssr/LayoutHelpTrigger.tsx +51 -0
  24. package/src/ssr/{LayoutPreferences.island.tsx → LayoutPreferences.tsx} +2 -2
  25. package/src/ssr/LayoutRail.tsx +15 -69
  26. package/src/ssr/MinimalLayout.tsx +2 -3
  27. package/src/ssr/MinimalLayoutPreferences.island.tsx +12 -15
  28. package/src/ssr/MobileNavigation.tsx +13 -8
  29. package/src/ssr/MobileProfileActions.tsx +10 -7
  30. package/src/ssr/PageTrailRow.tsx +14 -0
  31. package/src/ssr/{ProfilePreferences.island.tsx → ProfilePreferences.tsx} +12 -7
  32. package/src/ssr/PwaLayout.tsx +0 -2
  33. package/src/ssr/PwaRuntime.island.tsx +1 -1
  34. package/src/ssr/ShellRuntime.island.tsx +170 -0
  35. package/src/ssr/TimezoneCookie.island.tsx +3 -19
  36. package/src/ssr/WorkspaceNavigationProvider.tsx +20 -1
  37. package/src/ssr/announcements-dialog.tsx +66 -0
  38. package/src/ssr/{AppLaunchpad.island.tsx → app-launchpad.tsx} +42 -62
  39. package/src/ssr/busy-action.ts +19 -0
  40. package/src/ssr/islands/index.ts +9 -6
  41. package/src/ssr/launchpad-surface.tsx +15 -0
  42. package/src/ssr/layout-help-registry.tsx +60 -0
  43. package/src/ssr/mobile-navigation.browser-harness.tsx +20 -0
  44. package/src/ssr/preference-controller.ts +30 -21
  45. package/src/ssr/profile-actions.ts +26 -15
  46. package/src/ssr/profile-preferences-messages.ts +8 -4
  47. package/src/ssr/rail-context.ts +8 -12
  48. package/src/ssr/shell-data.tsx +48 -0
  49. package/src/ssr/timezone-cookie.ts +22 -0
  50. package/src/ssr/workspace-navigation.ts +25 -2
  51. package/src/styles/global.css +17 -5
  52. package/src/styles/utilities-navigation.css +13 -0
  53. package/src/workflows/language/compiler.ts +1 -1
  54. package/src/ssr/AppWorkspaceController.island.tsx +0 -21
  55. package/src/ssr/BrowserPushRegistration.island.tsx +0 -12
  56. package/src/ssr/GlobalAnnouncements.island.tsx +0 -117
  57. package/src/ssr/GlobalSearchTrigger.island.tsx +0 -75
  58. package/src/ssr/LayoutHelpTrigger.island.tsx +0 -61
  59. package/src/ssr/PageTrailRow.island.tsx +0 -20
  60. package/src/ssr/PageTrailRuntime.island.tsx +0 -26
  61. package/src/ssr/RegisteredHelpDocuments.island.tsx +0 -3
  62. /package/src/ssr/{LayoutBreadcrumbs.island.tsx → LayoutBreadcrumbs.tsx} +0 -0
  63. /package/src/ssr/{RailApps.island.tsx → RailApps.tsx} +0 -0
@@ -320,22 +320,33 @@ solid-island {
320
320
  edges, without padding, radius, shadow, or border. The rail keeps the app
321
321
  canvas, so the change of surface alone separates the two; no line does. The
322
322
  outermost workspace and the notices above it lose their frame, so nothing
323
- draws a box inside the app area. */
323
+ draws a box inside the app area.
324
+ The rail and the app area reach the window's edges also where the installed
325
+ app reports safe areas, such as the status bar and the home indicator. Their
326
+ own surfaces fill the insets, and their content stays inside them. */
324
327
  @media (min-width: 768px) and (max-width: 1535px) {
325
328
  .cloud-app-canvas[data-layout-authenticated] {
326
- padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px) env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px);
329
+ padding: 0;
327
330
  }
328
331
  .cloud-app-canvas[data-layout-authenticated] > .layout-rail {
329
- width: 3rem;
332
+ width: calc(3rem + env(safe-area-inset-left, 0px));
330
333
  margin-inline-start: 0;
331
- padding-bottom: var(--ui-space-shell);
334
+ padding: env(safe-area-inset-top, 0px) 0 calc(var(--ui-space-shell) + env(safe-area-inset-bottom, 0px))
335
+ env(safe-area-inset-left, 0px);
332
336
  }
333
337
  .cloud-app-canvas[data-layout-authenticated] > .layout-shell-content {
334
338
  gap: 0;
339
+ padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px) 0 0;
335
340
  background: var(--k2b-surface);
336
341
  }
342
+ /* A regular page scrolls `main`, so its end clears the home indicator. A
343
+ workspace reaches the bottom edge and clears it inside its own surfaces. */
344
+ .cloud-app-canvas[data-layout-authenticated] .layout-content-main:not(:has(.k2b-app-workspace)) {
345
+ padding-bottom: env(safe-area-inset-bottom, 0px);
346
+ }
337
347
  .cloud-app-canvas[data-layout-authenticated] .layout-content-main .k2b-app-workspace:not(.k2b-app-workspace *) {
338
348
  --k2b-workspace-radius: 0px;
349
+ --k2b-workspace-inset-bottom: env(safe-area-inset-bottom, 0px);
339
350
  border: 0;
340
351
  box-shadow: none;
341
352
  }
@@ -376,4 +387,5 @@ solid-island {
376
387
  width: 100%; white-space: normal; overflow-wrap: anywhere; font-size: .8125rem; line-height: 1.3;
377
388
  }
378
389
  .cloud-mobile-profile { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .5rem; border-top: 1px solid var(--ui-border); padding-top: 1rem; }
379
- .cloud-mobile-profile :is(a, button) { min-height: 2.75rem; white-space: normal; text-align: start; justify-content: flex-start; }
390
+ .cloud-mobile-profile > :is(a, button) { min-height: 2.75rem; white-space: normal; text-align: start; justify-content: flex-start; }
391
+ .cloud-mobile-profile__appearance { grid-column: 1 / -1; }
@@ -883,6 +883,19 @@
883
883
  transition: opacity 120ms ease-out;
884
884
  }
885
885
 
886
+ .layout-profile-preferences__appearance {
887
+ padding: 0.125rem 0.125rem 0.25rem;
888
+ }
889
+
890
+ .layout-profile-preferences__appearance .k2b-segmented-control {
891
+ width: 100%;
892
+ }
893
+
894
+ /* Content-sized segments: "Automatisch" needs more room than "Hell" in the narrow panel. */
895
+ .layout-profile-preferences__appearance .k2b-segmented-control__option {
896
+ flex: 1 1 auto;
897
+ }
898
+
886
899
  .dark .layout-profile-preferences__panel {
887
900
  color: var(--color-zinc-300);
888
901
  }
@@ -292,7 +292,7 @@ const compileSteps = (
292
292
  if (hasOwn(step, "switch")) {
293
293
  validateExactKeys(step, ["switch", "cases", "default"], stepPath, context);
294
294
  if (!Array.isArray(step.cases) || step.cases.length === 0) {
295
- addDiagnostic(context, "switch.cases", "switch requires at least one case", [...stepPath, "cases"]);
295
+ addDiagnostic(context, "switch.cases", "switch cases must be a non-empty list of { when, do } entries", [...stepPath, "cases"]);
296
296
  return;
297
297
  }
298
298
  const cases: Array<{ when: WorkflowJsonValue; steps: WorkflowIrStep[] }> = [];
@@ -1,21 +0,0 @@
1
- import { installAppWorkspaceController, serializeAppWorkspaceLayoutState } from "@k2b/ui";
2
- import { onCleanup, onMount } from "solid-js";
3
- import { appWorkspaceCookieName, readAppWorkspaceLayoutCookie } from "../_internal/app-workspace-state";
4
-
5
- const writeLayoutCookie = (appId: string | null | undefined, state: Parameters<typeof serializeAppWorkspaceLayoutState>[0]) => {
6
- if (!appId) return;
7
- const secure = window.location.protocol === "https:" ? "; Secure" : "";
8
- document.cookie = `${appWorkspaceCookieName(appId)}=${serializeAppWorkspaceLayoutState(state)}; Path=/; Max-Age=31536000; SameSite=Lax${secure}`;
9
- };
10
-
11
- export default function AppWorkspaceController(props: { appId?: string | null }) {
12
- let dispose = () => {};
13
- onMount(() => {
14
- dispose = installAppWorkspaceController({
15
- readState: () => readAppWorkspaceLayoutCookie(document.cookie, props.appId),
16
- writeState: (state) => writeLayoutCookie(props.appId, state),
17
- });
18
- });
19
- onCleanup(() => dispose());
20
- return null;
21
- }
@@ -1,12 +0,0 @@
1
- import { onMount } from "solid-js";
2
- import { browserNotificationClient } from "../browser/notifications";
3
-
4
- export default function BrowserPushRegistration() {
5
- onMount(() => {
6
- void browserNotificationClient.refreshExisting().catch((error) => {
7
- console.warn("[notifications] Failed to refresh the browser endpoint", error);
8
- });
9
- });
10
-
11
- return null;
12
- }
@@ -1,117 +0,0 @@
1
- import { Button, dialogCore, IconButton, MarkdownView, NoticeCard, PanelDialog, panelDialogOptions, useLocale } from "@k2b/ui";
2
- import { createSignal, For, onMount, Show } from "solid-js";
3
- import {
4
- ANNOUNCEMENTS_COOKIE,
5
- ANNOUNCEMENTS_COOKIE_MAX_AGE_SECONDS,
6
- type AnnouncementCookieState,
7
- mergeAnnouncementCookieState,
8
- serializeAnnouncementCookieState,
9
- } from "../contracts/announcement-cookie";
10
- import type { AnnouncementDisplayEntry } from "../contracts/announcements";
11
- import { platformMessages } from "./platform-messages";
12
-
13
- type Props = {
14
- banners: AnnouncementDisplayEntry[];
15
- announcements: AnnouncementDisplayEntry[];
16
- latestAnnouncementVersion: number;
17
- cookieState: AnnouncementCookieState;
18
- };
19
-
20
- const writeCookieState = (state: AnnouncementCookieState) => {
21
- document.cookie = `${ANNOUNCEMENTS_COOKIE}=${serializeAnnouncementCookieState(state)}; Path=/; Max-Age=${ANNOUNCEMENTS_COOKIE_MAX_AGE_SECONDS}; SameSite=Lax`;
22
- };
23
-
24
- export default function GlobalAnnouncements(props: Props) {
25
- const locale = useLocale();
26
- const t = () => platformMessages.resolve([locale()]).t;
27
- const [cookieState, setCookieState] = createSignal(props.cookieState);
28
- const [banners, setBanners] = createSignal(props.banners);
29
-
30
- const dismissBanner = (version: number) => {
31
- const next = mergeAnnouncementCookieState(cookieState(), { dismissedBannerVersions: [version] });
32
- setCookieState(next);
33
- writeCookieState(next);
34
- setBanners((items) => items.filter((item) => item.version !== version));
35
- };
36
-
37
- const closeAnnouncements = () => {
38
- const next = mergeAnnouncementCookieState(cookieState(), {
39
- seenAnnouncementVersion: props.latestAnnouncementVersion,
40
- });
41
- setCookieState(next);
42
- writeCookieState(next);
43
- };
44
-
45
- onMount(() => {
46
- if (props.announcements.length === 0) return;
47
-
48
- void dialogCore
49
- .open<void>(
50
- (close) => (
51
- <PanelDialog>
52
- <PanelDialog.Header
53
- title={t().announcements}
54
- subtitle={t().latestPlatformUpdates}
55
- icon="ti ti-speakerphone text-blue-500"
56
- close={() => close()}
57
- />
58
- <PanelDialog.Body>
59
- <For each={props.announcements}>
60
- {(entry) => (
61
- <article class="panel-dialog-section rounded-[var(--ui-radius-surface)] p-4">
62
- <div class="mb-3 flex items-start justify-between gap-3">
63
- <div class="min-w-0">
64
- <h2 class="text-base font-semibold text-primary">{entry.title}</h2>
65
- <p class="mt-0.5 text-xs text-dimmed">
66
- {new Date(entry.publishedAt).toLocaleDateString(locale(), {
67
- year: "numeric",
68
- month: "short",
69
- day: "numeric",
70
- })}
71
- </p>
72
- </div>
73
- <span class="rounded bg-zinc-100 px-1.5 py-0.5 text-[10px] font-medium text-dimmed dark:bg-zinc-800">
74
- v{entry.version}
75
- </span>
76
- </div>
77
- <MarkdownView trustedHtml={entry.bodyHtml} />
78
- </article>
79
- )}
80
- </For>
81
- </PanelDialog.Body>
82
- <PanelDialog.Footer>
83
- <span />
84
- <Button size="sm" onClick={() => close()}>
85
- {t().gotIt}
86
- </Button>
87
- </PanelDialog.Footer>
88
- </PanelDialog>
89
- ),
90
- panelDialogOptions,
91
- )
92
- .then(closeAnnouncements);
93
- });
94
-
95
- return (
96
- <Show when={banners().length > 0}>
97
- <div class="flex shrink-0 flex-col gap-1">
98
- <For each={banners()}>
99
- {(banner) => (
100
- <div class="cloud-announcement">
101
- <NoticeCard tone={banner.tone} title={banner.title}>
102
- <MarkdownView
103
- trustedHtml={banner.bodyHtml}
104
- headingScale="compact"
105
- class="-mx-2 max-h-36 overflow-y-auto overscroll-contain pl-2 pr-3 [&_p]:my-0"
106
- />
107
- </NoticeCard>
108
- <IconButton size="sm" label={t().dismissBanner} onClick={() => dismissBanner(banner.version)}>
109
- <i class="ti ti-x" aria-hidden="true" />
110
- </IconButton>
111
- </div>
112
- )}
113
- </For>
114
- </div>
115
- </Show>
116
- );
117
- }
@@ -1,75 +0,0 @@
1
- import { IconButton, Tooltip, useLocale } from "@k2b/ui";
2
- import { createEffect, onCleanup, onMount } from "solid-js";
3
- import { registerContextAwareCommand } from "../browser/command-bridge";
4
- import { attachCommandShortcuts } from "../browser/command-shortcuts";
5
- import type { NavigationSearchItem } from "../browser/navigation-search";
6
- import { importOnDemand } from "../browser/reload";
7
- import { openGlobalSearch } from "../browser/search";
8
- import { registerGlobalSearchHost } from "../browser/search-bridge";
9
- import { createGlobalSearchHost } from "./global-search-host";
10
- import { platformMessages } from "./platform-messages";
11
-
12
- type GlobalSearchTriggerProps = {
13
- variant: "header" | "rail" | "host";
14
- class?: string;
15
- registerCommand?: boolean;
16
- searchLinks?: NavigationSearchItem[];
17
- searchResources?: boolean;
18
- };
19
-
20
- /** Opens the spotlight-style global search dialog from nav/header trigger points. */
21
- export default function GlobalSearchTrigger(props: GlobalSearchTriggerProps) {
22
- const locale = useLocale();
23
- const t = () => platformMessages.resolve([locale()]).t;
24
-
25
- if (props.registerCommand) {
26
- const host = createGlobalSearchHost(() => props.searchLinks ?? [], props.searchResources);
27
- onMount(() => {
28
- onCleanup(registerGlobalSearchHost(host.open, host.dispose));
29
- });
30
- // Commands, their schemas and the capability client load on the first shortcut, not with every page.
31
- onMount(() =>
32
- onCleanup(
33
- attachCommandShortcuts(
34
- (command) =>
35
- void importOnDemand(() => import("../browser/commands")).then((commands) => commands?.runContextAwareCommand(command)),
36
- ),
37
- ),
38
- );
39
- createEffect(() =>
40
- onCleanup(
41
- registerContextAwareCommand({
42
- id: "cloud.search",
43
- title: t().openGlobalSearch,
44
- description: t().globalSearchDescription,
45
- icon: "ti ti-search",
46
- shortcut: "mod+k",
47
- action: { search: {} },
48
- }),
49
- ),
50
- );
51
- }
52
-
53
- if (props.variant === "host") return null;
54
-
55
- if (props.variant === "rail")
56
- return (
57
- <Tooltip.Trigger
58
- type="button"
59
- class={`rail-item text-blue-500 hover:bg-blue-500/10 hover:text-blue-600 dark:text-blue-400 dark:hover:bg-blue-500/15 dark:hover:text-blue-300 ${props.class ?? ""}`}
60
- onClick={() => openGlobalSearch()}
61
- aria-label={t().openGlobalSearch}
62
- placement="right"
63
- delay={0}
64
- content={t().searchShortcut}
65
- >
66
- <i class="ti ti-search text-base" />
67
- </Tooltip.Trigger>
68
- );
69
-
70
- return (
71
- <IconButton class={props.class} onClick={() => openGlobalSearch()} label={t().openGlobalSearch} title={t().searchShortcut}>
72
- <i class="ti ti-search text-base" />
73
- </IconButton>
74
- );
75
- }
@@ -1,61 +0,0 @@
1
- import { IconButton, Tooltip, useLocale } from "@k2b/ui";
2
- import { createEffect, onCleanup } from "solid-js";
3
- import { registerContextAwareCommand } from "../browser/command-bridge";
4
- import type { GlobalSearchHelpApp } from "./GlobalSearchHelpDialog";
5
- import { openLayoutHelpDialog } from "./LayoutHelp";
6
- import { platformMessages } from "./platform-messages";
7
-
8
- type LayoutHelpTriggerProps = {
9
- variant: "header" | "rail";
10
- class?: string;
11
- registerCommand?: boolean;
12
- searchHelpApps?: GlobalSearchHelpApp[];
13
- accent?: string;
14
- };
15
-
16
- /** Opens end-user help from either the desktop rail or the compact header. */
17
- export default function LayoutHelpTrigger(props: LayoutHelpTriggerProps) {
18
- const locale = useLocale();
19
- const t = () => platformMessages.resolve([locale()]).t;
20
- const searchHelpApps = props.searchHelpApps ?? [];
21
-
22
- const openHelp = () => {
23
- openLayoutHelpDialog(searchHelpApps, props.accent);
24
- };
25
-
26
- if (props.registerCommand) {
27
- createEffect(() =>
28
- onCleanup(
29
- registerContextAwareCommand({
30
- id: "cloud.help",
31
- title: t().openShortcutHelp,
32
- description: t().shortcutHelpDescription,
33
- icon: "ti ti-help-circle",
34
- shortcut: "shift+/",
35
- action: openHelp,
36
- }),
37
- ),
38
- );
39
- }
40
-
41
- if (props.variant === "rail")
42
- return (
43
- <Tooltip.Trigger
44
- type="button"
45
- class={`rail-item text-blue-500 hover:bg-blue-500/10 hover:text-blue-600 dark:text-blue-400 dark:hover:bg-blue-500/15 dark:hover:text-blue-300 ${props.class ?? ""}`}
46
- onClick={openHelp}
47
- aria-label={t().openHelp}
48
- placement="right"
49
- delay={0}
50
- content={t().helpShortcut}
51
- >
52
- <i class="ti ti-help-circle text-base" />
53
- </Tooltip.Trigger>
54
- );
55
-
56
- return (
57
- <IconButton class={props.class} onClick={openHelp} label={t().openHelp} title={t().helpShortcut}>
58
- <i class="ti ti-help-circle text-base" />
59
- </IconButton>
60
- );
61
- }
@@ -1,20 +0,0 @@
1
- import { PageTitle, usePageTrail } from "@k2b/ui";
2
- import { Show } from "solid-js";
3
-
4
- /**
5
- * The shell's own place for the way up, above pages without a workspace. It
6
- * shows only on a page with a page above it, and steps back when the page
7
- * renders a workspace (whose head shows it) or its own `PageTrail`.
8
- */
9
- export default function PageTrailRow() {
10
- const trail = usePageTrail();
11
- const deep = () => {
12
- const current = trail();
13
- return current?.ancestors.length ? current : undefined;
14
- };
15
- return (
16
- <div class="layout-page-head" data-trail={deep() ? "up" : undefined}>
17
- <Show when={deep()}>{(current) => <PageTitle trail={current()} as="p" fallback />}</Show>
18
- </div>
19
- );
20
- }
@@ -1,26 +0,0 @@
1
- import { followPageTrail, installPageTitleController, useLocale } from "@k2b/ui";
2
- import { onCleanup, onMount } from "solid-js";
3
- import { installPageTrailRuntime } from "./page-trail-runtime";
4
- import { platformMessages } from "./platform-messages";
5
-
6
- /**
7
- * One owner per document for the way up: it collapses every page title path
8
- * to its space, keeps server-rendered workspace heads current after
9
- * `layout.update()`, runs the per-tab memory behind up links, and offers
10
- * "One level up" (Alt+↑) while the page has a page above it.
11
- */
12
- export default function PageTrailRuntime() {
13
- const locale = useLocale();
14
- onMount(() => {
15
- const t = platformMessages.resolve([locale()]).t;
16
- const disposeTitles = installPageTitleController(document);
17
- const disposeFollow = followPageTrail(document);
18
- const disposeRuntime = installPageTrailRuntime((page) => ({ title: t.pageUp({ page }), description: t.pageUpDescription }));
19
- onCleanup(() => {
20
- disposeTitles();
21
- disposeFollow();
22
- disposeRuntime();
23
- });
24
- });
25
- return null;
26
- }
@@ -1,3 +0,0 @@
1
- import { LayoutHelpDocuments } from "./LayoutHelp";
2
-
3
- export default LayoutHelpDocuments;
File without changes