@k2b/cloud 0.32.0 → 0.34.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.
- package/package.json +5 -5
- package/src/_internal/app-presentation.ts +40 -11
- package/src/_internal/define-app.ts +18 -8
- package/src/_internal/page-responses.ts +5 -1
- package/src/_internal/registry.ts +10 -1
- package/src/_internal/widget-declarations.ts +16 -3
- package/src/_internal/widget-settings.ts +93 -0
- package/src/ai/capabilities.ts +8 -5
- package/src/ai/code-source-contracts.ts +17 -2
- package/src/ai/code-source-tools.ts +1 -0
- package/src/ai/default-tools.ts +4 -4
- package/src/ai/memories.ts +48 -1
- package/src/ai/memory-learning.ts +17 -6
- package/src/ai/memory-tool.ts +1 -0
- package/src/ai/migrate.ts +8 -0
- package/src/ai/provider.ts +4 -3
- package/src/ai/resource-refs.ts +35 -3
- package/src/ai/system-prompt.ts +3 -2
- package/src/api/index.ts +2 -0
- package/src/api/mcp.ts +48 -17
- package/src/api/pwa.ts +7 -2
- package/src/api/resource-links.ts +43 -0
- package/src/api/widgets.ts +163 -82
- package/src/browser/CloudResourceSearch.tsx +6 -6
- package/src/browser/command-shortcuts.ts +2 -11
- package/src/browser/navigation-search.ts +4 -1
- package/src/browser/resource-picker.tsx +1 -0
- package/src/browser/resource-search.ts +7 -2
- package/src/browser/widgets.ts +23 -5
- package/src/capabilities/testing.ts +13 -1
- package/src/cli/capabilities.ts +61 -17
- package/src/cli/index.ts +2 -0
- package/src/cli/resource-links.ts +13 -0
- package/src/contracts/app.ts +7 -1
- package/src/contracts/capabilities.ts +25 -5
- package/src/contracts/registry.ts +3 -1
- package/src/contracts/resource-href.ts +7 -0
- package/src/contracts/widgets.ts +80 -6
- package/src/desktop/index.ts +3 -2
- package/src/desktop/solid.tsx +7 -5
- package/src/server/widget-request.ts +34 -5
- package/src/services/app-approval.ts +4 -2
- package/src/services/approvals/index.ts +40 -11
- package/src/services/approvals/list.ts +1 -0
- package/src/services/approvals/migrate.ts +10 -0
- package/src/services/outbox.ts +13 -6
- package/src/services/pdf/markdown.ts +11 -7
- package/src/services/pwa-devices.ts +68 -15
- package/src/shared/ai-platform-prompt.ts +3 -2
- package/src/shared/app-presentation.ts +49 -1
- package/src/shared/index.ts +1 -0
- package/src/shared/markdown/callout-sanitizer.ts +37 -0
- package/src/shared/markdown/index.ts +8 -2
- package/src/shared/theme.ts +41 -20
- package/src/ssr/AnnouncementBanners.tsx +30 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +91 -0
- package/src/ssr/GlobalSearchTrigger.tsx +51 -0
- package/src/ssr/Layout.tsx +58 -99
- package/src/ssr/LayoutHeader.tsx +31 -77
- package/src/ssr/LayoutHelp.browser-harness.tsx +43 -0
- package/src/ssr/LayoutHelp.tsx +319 -238
- package/src/ssr/LayoutHelpTrigger.tsx +49 -0
- package/src/ssr/LayoutPreferences.tsx +31 -0
- package/src/ssr/LayoutRail.tsx +15 -69
- package/src/ssr/MinimalLayout.tsx +2 -3
- package/src/ssr/MinimalLayoutPreferences.island.tsx +6 -15
- package/src/ssr/MobileNavigation.tsx +13 -8
- package/src/ssr/MobileProfileActions.tsx +4 -6
- package/src/ssr/PageTrailRow.tsx +14 -0
- package/src/ssr/{ProfilePreferences.island.tsx → ProfilePreferences.tsx} +8 -5
- package/src/ssr/PwaLayout.tsx +0 -2
- package/src/ssr/PwaRuntime.island.tsx +1 -1
- package/src/ssr/ShellRuntime.island.tsx +166 -0
- package/src/ssr/ThemeCycleButton.tsx +46 -0
- package/src/ssr/TimezoneCookie.island.tsx +3 -19
- package/src/ssr/WorkspaceNavigationProvider.tsx +20 -1
- package/src/ssr/announcements-dialog.tsx +66 -0
- package/src/ssr/app-appearance.ts +6 -1
- package/src/ssr/{AppLaunchpad.island.tsx → app-launchpad.tsx} +42 -62
- package/src/ssr/busy-action.ts +19 -0
- package/src/ssr/islands/index.ts +9 -6
- package/src/ssr/launchpad-surface.tsx +15 -0
- package/src/ssr/layout-help-registry.tsx +60 -0
- package/src/ssr/layout-help-url.ts +39 -0
- package/src/ssr/mobile-navigation.browser-harness.tsx +20 -0
- package/src/ssr/platform-messages.ts +0 -8
- package/src/ssr/preference-controller.ts +62 -19
- package/src/ssr/profile-actions.ts +12 -5
- package/src/ssr/profile-preferences-messages.ts +10 -4
- package/src/ssr/rail-context.ts +8 -12
- package/src/ssr/rail-navigation.ts +3 -0
- package/src/ssr/shell-data.tsx +48 -0
- package/src/ssr/timezone-cookie.ts +22 -0
- package/src/ssr/workspace-navigation.ts +25 -2
- package/src/styles/effects.css +13 -3
- package/src/styles/global.css +43 -5
- package/src/styles/input.css +0 -8
- package/src/styles/tokens.css +0 -2
- package/src/styles/utilities-navigation.css +10 -0
- package/src/workflows/language/compiler.ts +1 -1
- package/src/ssr/AppWorkspaceController.island.tsx +0 -21
- package/src/ssr/BrowserPushRegistration.island.tsx +0 -12
- package/src/ssr/GlobalAnnouncements.island.tsx +0 -117
- package/src/ssr/GlobalSearchHelpDialog.tsx +0 -104
- package/src/ssr/GlobalSearchTrigger.island.tsx +0 -75
- package/src/ssr/LayoutHelpTrigger.island.tsx +0 -61
- package/src/ssr/LayoutPreferences.island.tsx +0 -21
- package/src/ssr/PageTrailRow.island.tsx +0 -20
- package/src/ssr/PageTrailRuntime.island.tsx +0 -26
- package/src/ssr/RegisteredHelpDocuments.island.tsx +0 -3
- /package/src/ssr/{LayoutBreadcrumbs.island.tsx → LayoutBreadcrumbs.tsx} +0 -0
- /package/src/ssr/{RailApps.island.tsx → RailApps.tsx} +0 -0
package/src/ssr/rail-context.ts
CHANGED
|
@@ -1,26 +1,22 @@
|
|
|
1
1
|
import type { RailPreferences, RailSnapshot } from "../contracts/rail-preferences";
|
|
2
2
|
import type { RailApp } from "./rail-navigation";
|
|
3
|
+
import { readShellData } from "./shell-data";
|
|
3
4
|
|
|
4
5
|
export type RailContext = { apps: RailApp[]; settings: RailSnapshot };
|
|
5
6
|
export const RAIL_PREFERENCES_EVENT = "cloud:rail-preferences";
|
|
6
7
|
|
|
7
8
|
export const readRailContext = (): RailContext | undefined => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (!text) return undefined;
|
|
11
|
-
// Framework-generated, escaped SSR data; never loaded from localStorage. The server validates the snapshot when
|
|
12
|
-
// it reads it, so the shell does not load the schemas on every page.
|
|
13
|
-
const value: RailContext = JSON.parse(text);
|
|
14
|
-
return { apps: value.apps, settings: value.settings };
|
|
9
|
+
const data = readShellData();
|
|
10
|
+
return data?.rail ? { apps: data.apps, settings: data.rail } : undefined;
|
|
15
11
|
};
|
|
16
12
|
|
|
17
13
|
export const publishRailPreferences = (settings: RailPreferences) => {
|
|
18
|
-
const
|
|
19
|
-
const element = document.getElementById("cloud-
|
|
20
|
-
if (
|
|
14
|
+
const data = readShellData();
|
|
15
|
+
const element = document.getElementById("cloud-shell-data");
|
|
16
|
+
if (data?.rail && element)
|
|
21
17
|
element.textContent = JSON.stringify({
|
|
22
|
-
...
|
|
23
|
-
|
|
18
|
+
...data,
|
|
19
|
+
rail: { ...settings, managedShortcuts: data.rail.managedShortcuts },
|
|
24
20
|
}).replace(/</g, "\\u003c");
|
|
25
21
|
window.dispatchEvent(new CustomEvent(RAIL_PREFERENCES_EVENT));
|
|
26
22
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RailPreferences, RailSnapshot } from "../contracts/rail-preferences";
|
|
2
|
+
import { helpPageAppId } from "./layout-help-url";
|
|
2
3
|
|
|
3
4
|
export type RailApp = {
|
|
4
5
|
id: string;
|
|
@@ -55,6 +56,8 @@ export const projectRailNavigation = (apps: readonly RailApp[], settings: RailSn
|
|
|
55
56
|
/** `currentPath` is the route as `pathname + search`; an absolute URL is accepted but never required. */
|
|
56
57
|
export const railLinkActive = (link: RailLink, currentPath: string): boolean => {
|
|
57
58
|
const current = new URL(currentPath, "https://cloud.invalid");
|
|
59
|
+
const helpAppId = helpPageAppId(current.pathname);
|
|
60
|
+
if (helpAppId && link.appId) return link.appId === helpAppId;
|
|
58
61
|
const target = new URL(link.match, current);
|
|
59
62
|
if (target.origin !== current.origin) return false;
|
|
60
63
|
if (link.exact) return target.pathname === current.pathname && target.search === current.search && target.hash === current.hash;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createContext, type JSX, useContext } from "solid-js";
|
|
2
|
+
import type { AppSearchLink } from "../contracts/app";
|
|
3
|
+
import type { RailSnapshot } from "../contracts/rail-preferences";
|
|
4
|
+
import type { CloudThemePreference } from "../shared/theme";
|
|
5
|
+
import type { AppLaunchpadLegalLink } from "./app-launchpad";
|
|
6
|
+
import type { RailApp } from "./rail-navigation";
|
|
7
|
+
|
|
8
|
+
export type ShellData = {
|
|
9
|
+
/** The authorized navigation catalog, sorted for the request locale: the rail, the app grid and the phone menu show it. */
|
|
10
|
+
apps: RailApp[];
|
|
11
|
+
/** Signed-in pages: the person's rail preferences. */
|
|
12
|
+
rail?: RailSnapshot;
|
|
13
|
+
legalLinks: AppLaunchpadLegalLink[];
|
|
14
|
+
/** Signed-in pages: the person, for the profile menu and the phone menu's profile actions. */
|
|
15
|
+
profile?: { name: string; avatarSrc?: string; appHref?: string };
|
|
16
|
+
theme: CloudThemePreference;
|
|
17
|
+
/** The current app's accent, for the Help dialog and the phone menu, which open outside the app canvas. */
|
|
18
|
+
accent?: string;
|
|
19
|
+
/** The search destinations of each app in `apps`, by app id in registry order; the search host expands them into rows. */
|
|
20
|
+
searchLinks: Record<string, readonly AppSearchLink[]>;
|
|
21
|
+
/** The current app, for links inside its Help. */
|
|
22
|
+
appId?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const ShellDataContext = createContext<() => ShellData>();
|
|
26
|
+
|
|
27
|
+
export function ShellDataProvider(props: { data: ShellData; children: JSX.Element }) {
|
|
28
|
+
return (
|
|
29
|
+
<ShellDataContext.Provider value={() => props.data}>
|
|
30
|
+
<script id="cloud-shell-data" type="application/json" innerHTML={JSON.stringify(props.data).replace(/</g, "\\u003c")} />
|
|
31
|
+
{props.children}
|
|
32
|
+
</ShellDataContext.Provider>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function readShellData(): ShellData | undefined {
|
|
37
|
+
if (typeof document === "undefined") return undefined;
|
|
38
|
+
const text = document.getElementById("cloud-shell-data")?.textContent;
|
|
39
|
+
if (!text) return undefined;
|
|
40
|
+
// Framework-generated, escaped SSR data; never loaded from localStorage. The server validates the rail snapshot when
|
|
41
|
+
// it reads it, so the shell does not load the schemas on every page.
|
|
42
|
+
const data: ShellData = JSON.parse(text);
|
|
43
|
+
return data;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function useShellData(): ShellData | undefined {
|
|
47
|
+
return useContext(ShellDataContext)?.() ?? readShellData();
|
|
48
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { cookies } from "@k2b/stdlib/browser";
|
|
2
|
+
import { TIMEZONE_COOKIE } from "../shared/time";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stores the browser's timezone for server-rendered times. With `reload`, a
|
|
6
|
+
* changed timezone reloads the page once, so it shows times in that zone.
|
|
7
|
+
*/
|
|
8
|
+
export const syncTimezoneCookie = (reload: boolean) => {
|
|
9
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
10
|
+
const current = cookies.readCookie(TIMEZONE_COOKIE);
|
|
11
|
+
if (!timeZone || current === timeZone) return;
|
|
12
|
+
cookies.writeCookie(TIMEZONE_COOKIE, timeZone);
|
|
13
|
+
if (!reload) return;
|
|
14
|
+
try {
|
|
15
|
+
if (sessionStorage.getItem("cloud.timezone.reload") !== timeZone) {
|
|
16
|
+
sessionStorage.setItem("cloud.timezone.reload", timeZone);
|
|
17
|
+
window.location.reload();
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
// Cookie persistence still succeeds; the next navigation will render with the browser timezone.
|
|
21
|
+
}
|
|
22
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createNavigation, type NavigationController, type NavigationItem } from "@k2b/ui";
|
|
1
|
+
import { type AppWorkspaceSidebarSwitcherProps, createNavigation, type NavigationController, type NavigationItem } from "@k2b/ui";
|
|
2
2
|
import { createEffect, onCleanup, onMount } from "solid-js";
|
|
3
3
|
|
|
4
4
|
export type WorkspaceNavigationState = { navigation: NavigationController; label: string; owner: Element };
|
|
@@ -44,6 +44,28 @@ function validItems(value: unknown): value is NavigationItem[] {
|
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function readSwitcher(value: unknown): AppWorkspaceSidebarSwitcherProps | undefined {
|
|
48
|
+
if (
|
|
49
|
+
!value ||
|
|
50
|
+
typeof value !== "object" ||
|
|
51
|
+
!("currentId" in value) ||
|
|
52
|
+
typeof value.currentId !== "string" ||
|
|
53
|
+
!("label" in value) ||
|
|
54
|
+
typeof value.label !== "string" ||
|
|
55
|
+
!("icon" in value) ||
|
|
56
|
+
typeof value.icon !== "string" ||
|
|
57
|
+
!("switchLabel" in value) ||
|
|
58
|
+
typeof value.switchLabel !== "string"
|
|
59
|
+
)
|
|
60
|
+
return;
|
|
61
|
+
const description = "description" in value ? value.description : undefined;
|
|
62
|
+
const color = "color" in value ? value.color : undefined;
|
|
63
|
+
if (description !== undefined && typeof description !== "string") return;
|
|
64
|
+
if (color !== undefined && typeof color !== "string") return;
|
|
65
|
+
// The snapshot carries only the head; options and actions wait for the owning island.
|
|
66
|
+
return { currentId: value.currentId, label: value.label, icon: value.icon, switchLabel: value.switchLabel, description, color };
|
|
67
|
+
}
|
|
68
|
+
|
|
47
69
|
export function readWorkspaceNavigation(): WorkspaceNavigationState | undefined {
|
|
48
70
|
if (typeof window === "undefined") return;
|
|
49
71
|
const current = window.__cloudWorkspaceNavigation;
|
|
@@ -67,10 +89,11 @@ export function readWorkspaceNavigation(): WorkspaceNavigationState | undefined
|
|
|
67
89
|
...(item.inlineActions ? { inlineActions: item.inlineActions.map((action) => ({ ...action, disabled: true })) } : {}),
|
|
68
90
|
}));
|
|
69
91
|
const items = links(value.items);
|
|
92
|
+
const switcher = "switcher" in value ? readSwitcher(value.switcher) : undefined;
|
|
70
93
|
return {
|
|
71
94
|
owner,
|
|
72
95
|
label: "label" in value && typeof value.label === "string" ? value.label : "",
|
|
73
|
-
navigation: createNavigation({ items: () => items }),
|
|
96
|
+
navigation: createNavigation({ items: () => items, ...(switcher ? { switcher: () => switcher } : {}) }),
|
|
74
97
|
};
|
|
75
98
|
} catch {
|
|
76
99
|
return;
|
package/src/styles/effects.css
CHANGED
|
@@ -345,12 +345,22 @@
|
|
|
345
345
|
background: color-mix(in srgb, var(--app-accent, var(--color-blue-500)) 10%, var(--ui-surface-muted));
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
+
/* The reader's layout steps follow its own width: a dialog, a full page, or
|
|
349
|
+
an app's embedded reader, never the window. */
|
|
350
|
+
.help-article {
|
|
351
|
+
container: help-article / inline-size;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.help-article-toc[data-empty] {
|
|
355
|
+
visibility: hidden;
|
|
356
|
+
}
|
|
357
|
+
|
|
348
358
|
.help-article-layout {
|
|
349
359
|
display: grid;
|
|
350
360
|
grid-template-columns: minmax(10rem, 13rem) minmax(0, 52rem);
|
|
351
361
|
justify-content: center;
|
|
352
362
|
align-items: start;
|
|
353
|
-
gap: clamp(2rem,
|
|
363
|
+
gap: clamp(2rem, 4cqi, 4rem);
|
|
354
364
|
}
|
|
355
365
|
|
|
356
366
|
.help-article-copy {
|
|
@@ -892,7 +902,7 @@
|
|
|
892
902
|
border: 0;
|
|
893
903
|
}
|
|
894
904
|
|
|
895
|
-
@
|
|
905
|
+
@container help-article (max-width: 56rem) {
|
|
896
906
|
.help-article-layout {
|
|
897
907
|
grid-template-columns: 3.25rem minmax(0, 52rem);
|
|
898
908
|
gap: 2rem;
|
|
@@ -919,7 +929,7 @@
|
|
|
919
929
|
}
|
|
920
930
|
}
|
|
921
931
|
|
|
922
|
-
@
|
|
932
|
+
@container help-article (max-width: 40rem) {
|
|
923
933
|
.help-article-layout {
|
|
924
934
|
grid-template-columns: minmax(0, 1fr);
|
|
925
935
|
gap: 1.5rem;
|
package/src/styles/global.css
CHANGED
|
@@ -303,6 +303,10 @@ solid-island {
|
|
|
303
303
|
so short and long pages keep the same width. Full-page surfaces own their
|
|
304
304
|
inner scrolling. */
|
|
305
305
|
html:has(.cloud-app-canvas:not([data-layout-full-page])) { scrollbar-gutter: stable; }
|
|
306
|
+
/* A page that scrolls its content in a ScrollArea grows with it here, so
|
|
307
|
+
the area does not scroll and reserves no second gutter beside the
|
|
308
|
+
document's. From md, `main` bounds its height and it scrolls. */
|
|
309
|
+
.cloud-app-canvas:not([data-layout-full-page]) .layout-content-main > .k2b-scroll-area { scrollbar-gutter: auto; }
|
|
306
310
|
.cloud-app-canvas > .layout-shell-content { gap: 0; }
|
|
307
311
|
.cloud-app-canvas .layout-header {
|
|
308
312
|
border-radius: 0; border: 0; border-bottom: 1px solid var(--ui-border);
|
|
@@ -316,26 +320,60 @@ solid-island {
|
|
|
316
320
|
|
|
317
321
|
}
|
|
318
322
|
|
|
323
|
+
/* Card: the outermost workspace is the app card, so it gets the card's radius
|
|
324
|
+
and frame also when it is flat. Its sidebar and content round their corners
|
|
325
|
+
with that radius and clip what they show. A workspace that is `main`'s own
|
|
326
|
+
child fills `main` and scrolls inside itself, so `main` neither scrolls nor
|
|
327
|
+
clips: it keeps no scrollbar gutter beside the card, the card keeps the
|
|
328
|
+
shell spacing on every side, and its frame shadow shows in that spacing. A
|
|
329
|
+
workspace nested in other page content leaves `main` the page scroller. For
|
|
330
|
+
signed-in people, the flush range below removes the frame again; it comes
|
|
331
|
+
later at equal specificity. */
|
|
332
|
+
@media (min-width: 768px) {
|
|
333
|
+
.cloud-app-canvas .layout-content-main:has(> .k2b-app-workspace, > :is(solid-island, solid-client) > .k2b-app-workspace) {
|
|
334
|
+
overflow: visible;
|
|
335
|
+
scrollbar-gutter: auto;
|
|
336
|
+
}
|
|
337
|
+
.cloud-app-canvas .layout-content-main .k2b-app-workspace[data-flat="true"]:not(.k2b-app-workspace *) {
|
|
338
|
+
--k2b-workspace-radius: var(--k2b-radius-dialog);
|
|
339
|
+
box-shadow: var(--k2b-shadow-frame);
|
|
340
|
+
}
|
|
341
|
+
.dark .cloud-app-canvas .layout-content-main .k2b-app-workspace[data-flat="true"]:not(.k2b-app-workspace *) {
|
|
342
|
+
border-color: var(--k2b-border);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
319
346
|
/* Flush: the app area is one opaque surface from the rail to the window's
|
|
320
347
|
edges, without padding, radius, shadow, or border. The rail keeps the app
|
|
321
348
|
canvas, so the change of surface alone separates the two; no line does. The
|
|
322
349
|
outermost workspace and the notices above it lose their frame, so nothing
|
|
323
|
-
draws a box inside the app area.
|
|
350
|
+
draws a box inside the app area.
|
|
351
|
+
The rail and the app area reach the window's edges also where the installed
|
|
352
|
+
app reports safe areas, such as the status bar and the home indicator. Their
|
|
353
|
+
own surfaces fill the insets, and their content stays inside them. */
|
|
324
354
|
@media (min-width: 768px) and (max-width: 1535px) {
|
|
325
355
|
.cloud-app-canvas[data-layout-authenticated] {
|
|
326
|
-
padding:
|
|
356
|
+
padding: 0;
|
|
327
357
|
}
|
|
328
358
|
.cloud-app-canvas[data-layout-authenticated] > .layout-rail {
|
|
329
|
-
width: 3rem;
|
|
359
|
+
width: calc(3rem + env(safe-area-inset-left, 0px));
|
|
330
360
|
margin-inline-start: 0;
|
|
331
|
-
padding
|
|
361
|
+
padding: env(safe-area-inset-top, 0px) 0 calc(var(--ui-space-shell) + env(safe-area-inset-bottom, 0px))
|
|
362
|
+
env(safe-area-inset-left, 0px);
|
|
332
363
|
}
|
|
333
364
|
.cloud-app-canvas[data-layout-authenticated] > .layout-shell-content {
|
|
334
365
|
gap: 0;
|
|
366
|
+
padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px) 0 0;
|
|
335
367
|
background: var(--k2b-surface);
|
|
336
368
|
}
|
|
369
|
+
/* A regular page scrolls `main`, so its end clears the home indicator. A
|
|
370
|
+
workspace reaches the bottom edge and clears it inside its own surfaces. */
|
|
371
|
+
.cloud-app-canvas[data-layout-authenticated] .layout-content-main:not(:has(.k2b-app-workspace)) {
|
|
372
|
+
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
373
|
+
}
|
|
337
374
|
.cloud-app-canvas[data-layout-authenticated] .layout-content-main .k2b-app-workspace:not(.k2b-app-workspace *) {
|
|
338
375
|
--k2b-workspace-radius: 0px;
|
|
376
|
+
--k2b-workspace-inset-bottom: env(safe-area-inset-bottom, 0px);
|
|
339
377
|
border: 0;
|
|
340
378
|
box-shadow: none;
|
|
341
379
|
}
|
|
@@ -376,4 +414,4 @@ solid-island {
|
|
|
376
414
|
width: 100%; white-space: normal; overflow-wrap: anywhere; font-size: .8125rem; line-height: 1.3;
|
|
377
415
|
}
|
|
378
416
|
.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; }
|
|
417
|
+
.cloud-mobile-profile > :is(a, button) { min-height: 2.75rem; white-space: normal; text-align: start; justify-content: flex-start; }
|
package/src/styles/input.css
CHANGED
|
@@ -38,10 +38,6 @@ button[disabled],
|
|
|
38
38
|
@apply text-white;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
input[type="checkbox"]:hover {
|
|
42
|
-
transform: var(--ui-check-hover-transform);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
41
|
input[type="checkbox"]:focus {
|
|
46
42
|
@apply outline-none;
|
|
47
43
|
}
|
|
@@ -53,8 +49,4 @@ button[disabled],
|
|
|
53
49
|
input[type="checkbox"]:disabled {
|
|
54
50
|
@apply opacity-50 cursor-not-allowed;
|
|
55
51
|
}
|
|
56
|
-
|
|
57
|
-
input[type="checkbox"]:disabled:hover {
|
|
58
|
-
transform: none;
|
|
59
|
-
}
|
|
60
52
|
}
|
package/src/styles/tokens.css
CHANGED
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
--ui-check-surface: var(--ui-field);
|
|
39
39
|
--ui-check-border: var(--ui-border-strong);
|
|
40
40
|
--ui-check-shadow: none;
|
|
41
|
-
--ui-check-hover-transform: none;
|
|
42
41
|
--ui-data-header: var(--ui-surface-subtle);
|
|
43
42
|
--ui-data-divider: var(--ui-border);
|
|
44
43
|
--ui-data-row-divider: color-mix(in srgb, var(--ui-border) 72%, transparent);
|
|
@@ -165,7 +164,6 @@
|
|
|
165
164
|
--ui-check-surface: var(--ui-field);
|
|
166
165
|
--ui-check-border: var(--ui-border-strong);
|
|
167
166
|
--ui-check-shadow: none;
|
|
168
|
-
--ui-check-hover-transform: none;
|
|
169
167
|
--ui-data-row-hover: rgb(255 255 255 / 0.045);
|
|
170
168
|
--ui-data-column-hover: rgb(255 255 255 / 0.018);
|
|
171
169
|
--ui-dropdown-menu-surface: var(--ui-surface-raised);
|
|
@@ -932,6 +932,16 @@
|
|
|
932
932
|
}
|
|
933
933
|
}
|
|
934
934
|
|
|
935
|
+
/* Reserves the width of the longest appearance label, so cycling never resizes the button. */
|
|
936
|
+
.cloud-theme-cycle__label { display: inline-grid; justify-items: start; }
|
|
937
|
+
.cloud-theme-cycle__label::before { content: attr(data-reserve-a); }
|
|
938
|
+
.cloud-theme-cycle__label::after { content: attr(data-reserve-b); }
|
|
939
|
+
.cloud-theme-cycle__label::before, .cloud-theme-cycle__label::after { height: 0; overflow: hidden; visibility: hidden; }
|
|
940
|
+
|
|
941
|
+
@media (width < 48rem) {
|
|
942
|
+
.cloud-theme-cycle__label[data-compact] { display: none; }
|
|
943
|
+
}
|
|
944
|
+
|
|
935
945
|
/* MinimalLayout stacks the page content and its footer in a column at least
|
|
936
946
|
one viewport tall, so the footer ends a short page and follows a long one.
|
|
937
947
|
Content that should fill the remaining height grows with flex-1. */
|
|
@@ -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
|
|
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,104 +0,0 @@
|
|
|
1
|
-
import { prompts, ScrollArea, useLocale } from "@k2b/ui";
|
|
2
|
-
import { createMemo, For, Show } from "solid-js";
|
|
3
|
-
import { platformMessages } from "./platform-messages";
|
|
4
|
-
|
|
5
|
-
export type GlobalSearchHelpApp = {
|
|
6
|
-
appId: string;
|
|
7
|
-
appName: string;
|
|
8
|
-
appIcon: string;
|
|
9
|
-
tags: string[];
|
|
10
|
-
help?: string;
|
|
11
|
-
tagHelp?: Array<{ tag: string; help: string }>;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
type GlobalSearchHelpDialogProps = { apps: GlobalSearchHelpApp[] };
|
|
15
|
-
|
|
16
|
-
export default function GlobalSearchHelpDialog(props: GlobalSearchHelpDialogProps) {
|
|
17
|
-
const locale = useLocale();
|
|
18
|
-
const t = () => platformMessages.resolve([locale()]).t;
|
|
19
|
-
const apps = createMemo(() =>
|
|
20
|
-
props.apps
|
|
21
|
-
.filter((app) => app.tags.length > 0)
|
|
22
|
-
.map((app) => ({
|
|
23
|
-
...app,
|
|
24
|
-
help: app.help?.trim() || undefined,
|
|
25
|
-
tags: [...new Set(app.tags.map((tag) => tag.toLowerCase()))].sort(),
|
|
26
|
-
tagHelp: [...new Map((app.tagHelp ?? []).map((entry) => [entry.tag.trim().toLowerCase(), entry.help.trim()])).entries()]
|
|
27
|
-
.filter(([tag, help]) => tag.length > 0 && help.length > 0)
|
|
28
|
-
.map(([tag, help]) => ({ tag, help })),
|
|
29
|
-
}))
|
|
30
|
-
.sort((a, b) => a.appName.localeCompare(b.appName)),
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<div class="flex max-h-[min(42rem,var(--ui-dialog-available-height))] min-h-0 flex-col gap-4 text-zinc-900 dark:text-zinc-100">
|
|
35
|
-
<div class="flex items-start justify-between gap-3">
|
|
36
|
-
<div>
|
|
37
|
-
<p class="text-sm text-dimmed">{t().searchTagsDescription({ tag: "#tag" })}</p>
|
|
38
|
-
</div>
|
|
39
|
-
</div>
|
|
40
|
-
|
|
41
|
-
<ScrollArea class="min-h-0 flex-1 pr-1">
|
|
42
|
-
<div class="flex flex-col gap-2">
|
|
43
|
-
<For each={apps()}>
|
|
44
|
-
{(app) => (
|
|
45
|
-
<section class="rounded-lg ring-1 ring-inset ring-zinc-200 dark:ring-zinc-800 p-3 bg-zinc-50/50 dark:bg-zinc-900/35">
|
|
46
|
-
<header class="flex items-center gap-2 text-sm">
|
|
47
|
-
<i class={app.appIcon} />
|
|
48
|
-
<span>{app.appName}</span>
|
|
49
|
-
</header>
|
|
50
|
-
<Show when={app.help}>
|
|
51
|
-
<p class="mt-1 text-xs text-dimmed">{app.help}</p>
|
|
52
|
-
</Show>
|
|
53
|
-
<Show
|
|
54
|
-
when={(app.tagHelp?.length ?? 0) > 0}
|
|
55
|
-
fallback={
|
|
56
|
-
<p class="mt-1 text-xs text-dimmed">
|
|
57
|
-
{t().tags}:{" "}
|
|
58
|
-
<For each={app.tags}>
|
|
59
|
-
{(tag, index) => (
|
|
60
|
-
<>
|
|
61
|
-
<code>#{tag}</code>
|
|
62
|
-
<Show when={index() < app.tags.length - 1}>, </Show>
|
|
63
|
-
</>
|
|
64
|
-
)}
|
|
65
|
-
</For>
|
|
66
|
-
</p>
|
|
67
|
-
}
|
|
68
|
-
>
|
|
69
|
-
<div class="mt-2 space-y-1 text-xs">
|
|
70
|
-
<For each={app.tagHelp}>
|
|
71
|
-
{(entry) => (
|
|
72
|
-
<div class="grid grid-cols-[6rem_minmax(0,1fr)] gap-2">
|
|
73
|
-
<code>#{entry.tag}</code>
|
|
74
|
-
<span class="text-dimmed">{entry.help}</span>
|
|
75
|
-
</div>
|
|
76
|
-
)}
|
|
77
|
-
</For>
|
|
78
|
-
</div>
|
|
79
|
-
</Show>
|
|
80
|
-
</section>
|
|
81
|
-
)}
|
|
82
|
-
</For>
|
|
83
|
-
<Show when={apps().length === 0}>
|
|
84
|
-
<div class="rounded-lg ring-1 ring-inset ring-zinc-200 dark:ring-zinc-800 p-3 text-xs text-dimmed bg-zinc-50/50 dark:bg-zinc-900/35">
|
|
85
|
-
{t().noAppSearchTags}
|
|
86
|
-
</div>
|
|
87
|
-
</Show>
|
|
88
|
-
</div>
|
|
89
|
-
</ScrollArea>
|
|
90
|
-
</div>
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export const openGlobalSearchHelpDialog = (
|
|
95
|
-
apps: GlobalSearchHelpApp[],
|
|
96
|
-
locale = typeof document === "undefined" ? "en" : document.documentElement.lang || "en",
|
|
97
|
-
) => {
|
|
98
|
-
const t = platformMessages.resolve([locale]).t;
|
|
99
|
-
void prompts.dialog<void>(() => <GlobalSearchHelpDialog apps={apps} />, {
|
|
100
|
-
title: t.searchTags,
|
|
101
|
-
icon: "ti ti-help-circle",
|
|
102
|
-
size: "large",
|
|
103
|
-
});
|
|
104
|
-
};
|