@k2b/cloud 0.28.0 → 0.29.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 +7 -5
- package/src/_internal/cli-plugins.ts +18 -0
- package/src/_internal/define-app.ts +21 -5
- package/src/_internal/registry-validation.ts +30 -1
- package/src/_internal/runtime-context.ts +1 -0
- package/src/access/PermissionEditor.tsx +11 -4
- package/src/ai/assistant-models.ts +25 -4
- package/src/ai/audio-tool.ts +3 -10
- package/src/ai/capabilities.ts +35 -9
- package/src/ai/chat/blocks.tsx +97 -17
- package/src/ai/chat/builtin-tools.tsx +3 -1
- package/src/ai/chat/composer-adapter.ts +8 -0
- package/src/ai/chat/live-turn.browser-harness.tsx +12 -3
- package/src/ai/chat/message-actions.tsx +1 -1
- package/src/ai/chat/message-utils.ts +28 -6
- package/src/ai/chat/messages.ts +16 -0
- package/src/ai/chat/presentation.tsx +1 -8
- package/src/ai/chat/tool-names.ts +157 -0
- package/src/ai/chat/turn-layout.ts +12 -0
- package/src/ai/chat/turn-view.tsx +15 -7
- package/src/ai/chat-tasks.ts +16 -3
- package/src/ai/code-mode-skill.ts +4 -4
- package/src/ai/compaction.ts +22 -3
- package/src/ai/default-tools.ts +9 -2
- package/src/ai/executor.ts +127 -40
- package/src/ai/file-media-type.ts +4 -0
- package/src/ai/files-store.ts +17 -10
- package/src/ai/http.ts +11 -10
- package/src/ai/index.ts +1 -0
- package/src/ai/memories.ts +2 -0
- package/src/ai/memory-learning.ts +59 -5
- package/src/ai/memory-workflow-evidence.ts +35 -10
- package/src/ai/projects-routes.ts +5 -1
- package/src/ai/projects.ts +5 -1
- package/src/ai/protocol.ts +34 -1
- package/src/ai/provider.ts +16 -1
- package/src/ai/quota-provider.ts +4 -1
- package/src/ai/routes.ts +20 -10
- package/src/ai/settings.ts +54 -17
- package/src/ai/skill-seeds.ts +11 -3
- package/src/ai/store.ts +56 -20
- package/src/ai/structured.ts +2 -1
- package/src/ai/system-prompt.ts +5 -1
- package/src/ai/transcription.ts +58 -13
- package/src/ai/turn-failure.ts +5 -1
- package/src/ai/turn-guidance.ts +505 -0
- package/src/ai/types.ts +1 -0
- package/src/ai/ui.tsx +1 -0
- package/src/api/admin-core-settings.ts +16 -1
- package/src/api/me.ts +42 -1
- package/src/browser/FileChooser.tsx +71 -295
- package/src/browser/FileSaver.tsx +407 -0
- package/src/browser/app-badges.ts +16 -0
- package/src/browser/choose-files.browser-harness.tsx +22 -6
- package/src/browser/choose-files.tsx +23 -36
- package/src/browser/file-chooser-messages.ts +64 -0
- package/src/browser/file-providers.ts +258 -8
- package/src/browser/files.tsx +26 -3
- package/src/browser/notifications.ts +37 -1
- package/src/browser/provider-folder.tsx +301 -0
- package/src/browser/provider-list.ts +39 -0
- package/src/browser/save-files.browser-harness.tsx +35 -0
- package/src/browser/save-files.tsx +107 -0
- package/src/contracts/app.ts +10 -0
- package/src/contracts/commands.ts +3 -1
- package/src/contracts/notification-group.ts +6 -0
- package/src/contracts/notification-types.ts +6 -0
- package/src/contracts/registry.ts +1 -0
- package/src/contracts/user-notifications.ts +74 -0
- package/src/server/middleware/request-logger.ts +11 -2
- package/src/services/accounts/avatar.ts +5 -2
- package/src/services/image-metadata.ts +438 -0
- package/src/services/index.ts +1 -0
- package/src/services/logging/redaction.ts +50 -0
- package/src/services/mandates/index.ts +4 -2
- package/src/services/mandates/policy.ts +20 -0
- package/src/services/notifications/browser.ts +48 -15
- package/src/services/notifications/dispatcher.ts +31 -1
- package/src/services/notifications/platform.ts +32 -4
- package/src/services/notifications/preview.ts +23 -0
- package/src/services/notifications/quiet.ts +153 -0
- package/src/services/notifications/user.ts +6 -0
- package/src/services/pdf/template-preview.ts +2 -1
- package/src/services/settings/app.ts +3 -0
- package/src/services/settings/store.ts +4 -1
- package/src/services/weather/geo.ts +6 -2
- package/src/shared/ai-platform-prompt.ts +2 -2
- package/src/shared/redirect.ts +2 -1
- package/src/shared/template-rendering.ts +120 -7
- package/src/ssr/AppBadge.tsx +30 -0
- package/src/ssr/AppLaunchpadPanel.tsx +47 -26
- package/src/ssr/IslandError.tsx +33 -0
- package/src/ssr/Layout.tsx +3 -0
- package/src/ssr/LayoutHelp.tsx +4 -2
- package/src/ssr/RailApps.island.tsx +9 -2
- package/src/ssr/app-badges.ts +93 -0
- package/src/ssr/rail-messages.ts +4 -0
- package/src/ssr/rail-navigation.ts +2 -0
- package/src/styles/effects.css +23 -0
- package/src/styles/file-chooser.css +25 -0
- package/src/styles/utilities-navigation.css +41 -0
|
@@ -3,6 +3,7 @@ import { IconButton, ScrollArea, TextInput, useLocale } from "@k2b/ui";
|
|
|
3
3
|
import { createSignal, For, type JSX, Show } from "solid-js";
|
|
4
4
|
import { Dynamic } from "solid-js/web";
|
|
5
5
|
import type { CloudTheme } from "../shared/theme";
|
|
6
|
+
import { AppBadge, useAppBadgeDescription } from "./AppBadge";
|
|
6
7
|
import { MobileProfileActions } from "./MobileProfileActions";
|
|
7
8
|
import { platformMessages } from "./platform-messages";
|
|
8
9
|
import { openRailEditor } from "./RailEditor";
|
|
@@ -17,6 +18,8 @@ export type AppLaunchpadApp = {
|
|
|
17
18
|
href: string;
|
|
18
19
|
description?: string;
|
|
19
20
|
accent?: string;
|
|
21
|
+
/** The app's `nav.badge` endpoint. */
|
|
22
|
+
badge?: string;
|
|
20
23
|
};
|
|
21
24
|
|
|
22
25
|
export type AppLaunchpadLegalLink = {
|
|
@@ -119,39 +122,57 @@ export const AppLaunchpadPanel = (
|
|
|
119
122
|
<section class="mb-5" aria-label={t().shortcuts}>
|
|
120
123
|
<div class="flex flex-wrap justify-center gap-3">
|
|
121
124
|
<For each={shortcuts()}>
|
|
122
|
-
{(shortcut) =>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
125
|
+
{(shortcut) => {
|
|
126
|
+
const badge = useAppBadgeDescription(() => shortcut.badge);
|
|
127
|
+
return (
|
|
128
|
+
<a
|
|
129
|
+
onClick={follow}
|
|
130
|
+
href={shortcut.href}
|
|
131
|
+
class="group flex w-16 min-w-0 flex-col items-center gap-1 rounded-lg p-1 text-center focus-ui"
|
|
132
|
+
>
|
|
133
|
+
<span class="relative grid h-9 w-9 place-items-center rounded-xl bg-[var(--ui-hover)] text-lg transition-colors group-hover:bg-[var(--ui-active)]">
|
|
134
|
+
<i class={shortcut.iconClass} aria-hidden="true" />
|
|
135
|
+
<AppBadge endpoint={shortcut.badge} />
|
|
136
|
+
</span>
|
|
137
|
+
<span class="max-w-full truncate text-[11px] font-medium text-primary dark:text-white">
|
|
138
|
+
{shortcut.label}
|
|
139
|
+
<Show when={badge()}>
|
|
140
|
+
<span class="sr-only">, {badge()}</span>
|
|
141
|
+
</Show>
|
|
142
|
+
</span>
|
|
143
|
+
</a>
|
|
144
|
+
);
|
|
145
|
+
}}
|
|
134
146
|
</For>
|
|
135
147
|
</div>
|
|
136
148
|
</section>
|
|
137
149
|
</Show>
|
|
138
150
|
<div class="launchpad-apps-grid flex flex-wrap justify-center gap-x-4 gap-y-4 sm:gap-x-7 sm:gap-y-6">
|
|
139
151
|
<For each={apps()}>
|
|
140
|
-
{(app) =>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
class="app-icon grid h-12 w-12 place-items-center rounded-[0.95rem] text-[1.25rem] sm:h-16 sm:w-16 sm:rounded-[1.25rem] sm:text-[1.7rem]"
|
|
148
|
-
style={appIconStyle(app)}
|
|
152
|
+
{(app) => {
|
|
153
|
+
const badge = useAppBadgeDescription(() => app.badge);
|
|
154
|
+
return (
|
|
155
|
+
<a
|
|
156
|
+
onClick={follow}
|
|
157
|
+
href={app.href}
|
|
158
|
+
class="launchpad-app group flex w-[4.75rem] min-w-0 flex-col items-center gap-1.5 rounded-2xl p-1 text-center focus-ui sm:w-[6.25rem] sm:gap-2 sm:p-2"
|
|
149
159
|
>
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
160
|
+
<span
|
|
161
|
+
class="app-icon relative grid h-12 w-12 place-items-center rounded-[0.95rem] text-[1.25rem] sm:h-16 sm:w-16 sm:rounded-[1.25rem] sm:text-[1.7rem]"
|
|
162
|
+
style={appIconStyle(app)}
|
|
163
|
+
>
|
|
164
|
+
<i class={app.iconClass} />
|
|
165
|
+
<AppBadge endpoint={app.badge} />
|
|
166
|
+
</span>
|
|
167
|
+
<span class="max-w-full truncate text-[11px] font-medium text-primary sm:text-xs dark:text-white">
|
|
168
|
+
{app.label}
|
|
169
|
+
<Show when={badge()}>
|
|
170
|
+
<span class="sr-only">, {badge()}</span>
|
|
171
|
+
</Show>
|
|
172
|
+
</span>
|
|
173
|
+
</a>
|
|
174
|
+
);
|
|
175
|
+
}}
|
|
155
176
|
</For>
|
|
156
177
|
</div>
|
|
157
178
|
<Show when={!apps().length && !shortcuts().length}>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { IslandErrorProps } from "@k2b/ssr";
|
|
2
|
+
import { i18n } from "@k2b/stdlib";
|
|
3
|
+
import { Button, Placeholder, useLocale } from "@k2b/ui";
|
|
4
|
+
|
|
5
|
+
const messages = i18n.define({
|
|
6
|
+
baseLocale: "en",
|
|
7
|
+
messages: {
|
|
8
|
+
en: { title: "This section could not be displayed.", retry: "Try again" },
|
|
9
|
+
de: { title: "Dieser Bereich konnte nicht angezeigt werden.", retry: "Erneut versuchen" },
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Takes the place of an island or client component whose rendering failed.
|
|
15
|
+
* `defineApp` hands it to @k2b/ssr as `errorFallback`, so every island of every
|
|
16
|
+
* app gets it; the locale comes from `<html lang>`. "Try again" remounts the
|
|
17
|
+
* island with its original props.
|
|
18
|
+
*/
|
|
19
|
+
export default function IslandError(props: IslandErrorProps) {
|
|
20
|
+
const locale = useLocale();
|
|
21
|
+
const t = () => messages.resolve([locale()]).t;
|
|
22
|
+
return (
|
|
23
|
+
<Placeholder
|
|
24
|
+
state="error"
|
|
25
|
+
title={t().title}
|
|
26
|
+
action={
|
|
27
|
+
<Button variant="secondary" onClick={() => props.reset()}>
|
|
28
|
+
{t().retry}
|
|
29
|
+
</Button>
|
|
30
|
+
}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
}
|
package/src/ssr/Layout.tsx
CHANGED
|
@@ -61,6 +61,7 @@ function buildNavLinks(
|
|
|
61
61
|
match: resolveNavMatch(app) ?? app.nav.href.split("?")[0] ?? app.nav.href,
|
|
62
62
|
description: app.description,
|
|
63
63
|
accent: app.appearance?.accent,
|
|
64
|
+
badge: app.nav.badge,
|
|
64
65
|
} satisfies LayoutAppLink,
|
|
65
66
|
}));
|
|
66
67
|
const primary = links.filter((entry) => entry.section === "primary").map((entry) => entry.link);
|
|
@@ -75,6 +76,7 @@ function buildNavLinks(
|
|
|
75
76
|
match: "/admin",
|
|
76
77
|
description: t.platformAdministration,
|
|
77
78
|
accent: undefined,
|
|
79
|
+
badge: undefined,
|
|
78
80
|
});
|
|
79
81
|
}
|
|
80
82
|
return { primary, more };
|
|
@@ -166,6 +168,7 @@ export default function Layout(props: LayoutProps) {
|
|
|
166
168
|
href: app.href,
|
|
167
169
|
description: app.description,
|
|
168
170
|
accent: app.accent,
|
|
171
|
+
badge: app.badge,
|
|
169
172
|
}));
|
|
170
173
|
const searchLinks = navigationSearchItems(
|
|
171
174
|
runtime.apps,
|
package/src/ssr/LayoutHelp.tsx
CHANGED
|
@@ -95,11 +95,13 @@ const registerTopic = (topic: HelpTopic) => {
|
|
|
95
95
|
/** Register an app's server-prepared Markdown manifest with the shared help UI. */
|
|
96
96
|
export function LayoutHelpDocuments(props: LayoutHelpDocumentsProps) {
|
|
97
97
|
onMount(() => {
|
|
98
|
-
|
|
98
|
+
// Cleanup runs while this unmounts, when a parent `<Show>` may already have dropped the props' source.
|
|
99
|
+
const pageBase = props.pageBase;
|
|
100
|
+
window.__cloudLayoutHelpPageBase = pageBase;
|
|
99
101
|
const disposers = props.documents.map((document) => registerTopic({ ...document, kind: "document" }));
|
|
100
102
|
onCleanup(() => {
|
|
101
103
|
disposers.forEach((dispose) => dispose());
|
|
102
|
-
if (window.__cloudLayoutHelpPageBase ===
|
|
104
|
+
if (window.__cloudLayoutHelpPageBase === pageBase) delete window.__cloudLayoutHelpPageBase;
|
|
103
105
|
});
|
|
104
106
|
});
|
|
105
107
|
return null;
|
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import { ScrollArea, Tooltip, useLocale } from "@k2b/ui";
|
|
2
2
|
import { createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js";
|
|
3
3
|
import type { RailSnapshot } from "../contracts/rail-preferences";
|
|
4
|
+
import { AppBadge, useAppBadgeDescription } from "./AppBadge";
|
|
4
5
|
import { appAccentStyle } from "./app-appearance";
|
|
6
|
+
import { watchAppBadges } from "./app-badges";
|
|
5
7
|
import { RAIL_PREFERENCES_EVENT, readRailContext } from "./rail-context";
|
|
6
8
|
import { projectRailNavigation, type RailApp, type RailLink, railLinkActive } from "./rail-navigation";
|
|
7
9
|
|
|
8
10
|
function RailIcon(props: { link: RailLink; currentPath: string }) {
|
|
9
11
|
let target: HTMLAnchorElement | undefined;
|
|
10
12
|
const active = () => railLinkActive(props.link, props.currentPath);
|
|
13
|
+
const badge = useAppBadgeDescription(() => props.link.badge);
|
|
11
14
|
return (
|
|
12
15
|
<>
|
|
13
16
|
<a
|
|
14
17
|
ref={target}
|
|
15
18
|
href={props.link.href}
|
|
16
|
-
class={`rail-item shrink-0 ${active() ? "rail-item-active" : ""}`}
|
|
17
|
-
aria-label={props.link.label}
|
|
19
|
+
class={`rail-item relative shrink-0 ${active() ? "rail-item-active" : ""}`}
|
|
20
|
+
aria-label={badge() ? `${props.link.label}, ${badge()}` : props.link.label}
|
|
18
21
|
aria-current={active() ? "page" : undefined}
|
|
19
22
|
style={appAccentStyle(props.link.accent)}
|
|
20
23
|
>
|
|
21
24
|
<i class={`${props.link.iconClass} text-base`} aria-hidden="true" />
|
|
22
25
|
<span class="sr-only">{props.link.label}</span>
|
|
26
|
+
<AppBadge endpoint={props.link.badge} />
|
|
23
27
|
</a>
|
|
24
28
|
<Tooltip target={() => target} content={props.link.label} placement="right" delay={0} />
|
|
25
29
|
</>
|
|
@@ -36,10 +40,13 @@ export default function RailApps(props: { apps: RailApp[]; settings: RailSnapsho
|
|
|
36
40
|
const syncLocation = () => setCurrentPath(`${window.location.pathname}${window.location.search}${window.location.hash}`);
|
|
37
41
|
sync();
|
|
38
42
|
syncLocation();
|
|
43
|
+
// Every visible app, not only the rail's: the app grid shows the same counts.
|
|
44
|
+
const stopBadges = watchAppBadges(props.apps.flatMap((app) => (app.badge ? [app.badge] : [])));
|
|
39
45
|
window.addEventListener(RAIL_PREFERENCES_EVENT, sync);
|
|
40
46
|
window.addEventListener("popstate", syncLocation);
|
|
41
47
|
window.addEventListener("hashchange", syncLocation);
|
|
42
48
|
onCleanup(() => {
|
|
49
|
+
stopBadges();
|
|
43
50
|
window.removeEventListener(RAIL_PREFERENCES_EVENT, sync);
|
|
44
51
|
window.removeEventListener("popstate", syncLocation);
|
|
45
52
|
window.removeEventListener("hashchange", syncLocation);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createSignal } from "solid-js";
|
|
2
|
+
import { APP_BADGES_REFRESH_EVENT } from "../browser/app-badges";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shortest time between two scheduled reads in one tab, and the longest one
|
|
6
|
+
* read may take. Badges are a hint to open an app, not a live view, so one
|
|
7
|
+
* read per minute keeps them current while someone works elsewhere without a
|
|
8
|
+
* steady stream of requests per tab.
|
|
9
|
+
*/
|
|
10
|
+
export const APP_BADGE_INTERVAL_MS = 60_000;
|
|
11
|
+
|
|
12
|
+
// Keyed by endpoint, so a pinned app shortcut and the app's own entry share one
|
|
13
|
+
// count. Only the browser ever writes it; server renders always see no badge.
|
|
14
|
+
const [counts, setCounts] = createSignal<ReadonlyMap<string, number>>(new Map());
|
|
15
|
+
|
|
16
|
+
/** The count for a declared `nav.badge` endpoint; 0 until the browser has read it. */
|
|
17
|
+
export const appBadgeCount = (endpoint: string | undefined): number => (endpoint ? (counts().get(endpoint) ?? 0) : 0);
|
|
18
|
+
|
|
19
|
+
/** A positive safe integer from an `AppNavBadge` body, else 0. */
|
|
20
|
+
export const parseAppBadge = (body: unknown): number => {
|
|
21
|
+
if (typeof body !== "object" || body === null || !("count" in body)) return 0;
|
|
22
|
+
const count = body.count;
|
|
23
|
+
return typeof count === "number" && Number.isSafeInteger(count) && count > 0 ? count : 0;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const readBadge = async (endpoint: string, signal: AbortSignal): Promise<number> => {
|
|
27
|
+
try {
|
|
28
|
+
const response = await fetch(endpoint, { credentials: "same-origin", headers: { accept: "application/json" }, signal });
|
|
29
|
+
return response.status === 200 ? parseAppBadge(await response.json()) : 0;
|
|
30
|
+
} catch {
|
|
31
|
+
// An unreachable or failing app shows no badge rather than a stale one.
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
let watching = false;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Reads the badge endpoints after hydration, every interval while the tab is
|
|
40
|
+
* visible, when a hidden tab becomes visible again after the interval, and on
|
|
41
|
+
* `refreshAppBadges()`. A hidden tab sends nothing and keeps its schedule.
|
|
42
|
+
* Each endpoint has at most one read in flight, and its count shows as soon as
|
|
43
|
+
* that read settles, so a slow app delays only its own badge. A refresh while
|
|
44
|
+
* a read runs queues one more read after it instead of another request, and a
|
|
45
|
+
* read still running after the interval gives up. Returns a disposer.
|
|
46
|
+
*/
|
|
47
|
+
export const watchAppBadges = (endpoints: readonly string[]): (() => void) => {
|
|
48
|
+
const unique = [...new Set(endpoints)];
|
|
49
|
+
if (typeof window === "undefined" || unique.length === 0 || watching) return () => {};
|
|
50
|
+
watching = true;
|
|
51
|
+
const stopped = new AbortController();
|
|
52
|
+
// Endpoints with a read in flight, mapped to whether one more read waits for it.
|
|
53
|
+
const running = new Map<string, boolean>();
|
|
54
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
55
|
+
let last = Number.NEGATIVE_INFINITY;
|
|
56
|
+
|
|
57
|
+
const read = (endpoint: string) => {
|
|
58
|
+
if (running.has(endpoint)) {
|
|
59
|
+
running.set(endpoint, true);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
running.set(endpoint, false);
|
|
63
|
+
void readBadge(endpoint, AbortSignal.any([stopped.signal, AbortSignal.timeout(APP_BADGE_INTERVAL_MS)])).then((count) => {
|
|
64
|
+
if (stopped.signal.aborted) return;
|
|
65
|
+
setCounts((previous) => new Map(previous).set(endpoint, count));
|
|
66
|
+
const queued = running.get(endpoint);
|
|
67
|
+
running.delete(endpoint);
|
|
68
|
+
if (queued && document.visibilityState !== "hidden") read(endpoint);
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
const refresh = () => {
|
|
72
|
+
// Leave the pending read in place, so the tab still reads on time once it is visible again.
|
|
73
|
+
if (document.visibilityState === "hidden") return;
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
last = Date.now();
|
|
76
|
+
timer = setTimeout(refresh, APP_BADGE_INTERVAL_MS);
|
|
77
|
+
for (const endpoint of unique) read(endpoint);
|
|
78
|
+
};
|
|
79
|
+
const resume = () => {
|
|
80
|
+
if (document.visibilityState !== "hidden" && Date.now() - last >= APP_BADGE_INTERVAL_MS) refresh();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
document.addEventListener("visibilitychange", resume);
|
|
84
|
+
window.addEventListener(APP_BADGES_REFRESH_EVENT, refresh);
|
|
85
|
+
refresh();
|
|
86
|
+
return () => {
|
|
87
|
+
watching = false;
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
stopped.abort();
|
|
90
|
+
document.removeEventListener("visibilitychange", resume);
|
|
91
|
+
window.removeEventListener(APP_BADGES_REFRESH_EVENT, refresh);
|
|
92
|
+
};
|
|
93
|
+
};
|
package/src/ssr/rail-messages.ts
CHANGED
|
@@ -32,6 +32,8 @@ export const railMessages = i18n.define({
|
|
|
32
32
|
saveFailed: "Could not save app bar settings. Your edits are still here.",
|
|
33
33
|
conflict: "The app bar changed in another editor. Cancel and reopen this editor to load the latest settings.",
|
|
34
34
|
invalid: "Check shortcut titles, links and icons. Each app can be pinned once; settings must fit within 16 KiB.",
|
|
35
|
+
badge: ({ count }: { count: number }) => `${count} new`,
|
|
36
|
+
badgeMany: "more than 99 new",
|
|
35
37
|
},
|
|
36
38
|
de: {
|
|
37
39
|
managed: "Von der Administration bereitgestellt",
|
|
@@ -63,6 +65,8 @@ export const railMessages = i18n.define({
|
|
|
63
65
|
conflict:
|
|
64
66
|
"Die App-Leiste wurde in einem anderen Editor geändert. Brich ab und öffne den Editor erneut, um den aktuellen Stand zu laden.",
|
|
65
67
|
invalid: "Prüfe Titel, Links und Icons. Jede App kann einmal angeheftet werden; die Einstellungen dürfen höchstens 16 KiB belegen.",
|
|
68
|
+
badge: ({ count }) => `${count} neu`,
|
|
69
|
+
badgeMany: "mehr als 99 neu",
|
|
66
70
|
},
|
|
67
71
|
},
|
|
68
72
|
});
|
package/src/styles/effects.css
CHANGED
|
@@ -892,3 +892,26 @@ dialog:has(.launchpad-panel)::backdrop {
|
|
|
892
892
|
.k2b-ui .ai-response-info__rows dd { min-width: 0; margin: 0; overflow: hidden; color: var(--k2b-text); text-align: end; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
893
893
|
.k2b-ui .ai-response-info__tools { display: flex; flex-wrap: wrap; gap: 0.25rem 1rem; margin: 0; padding: 0; list-style: none; font-size: 0.8125rem; color: var(--k2b-text-secondary); }
|
|
894
894
|
.k2b-ui .ai-response-info__tools > li { display: inline-flex; align-items: center; gap: 0.375rem; }
|
|
895
|
+
|
|
896
|
+
/* The image a step looked at: a small inset thumbnail whose box is fixed before the image loads, so nothing moves. */
|
|
897
|
+
.k2b-ui .k2b-chat-activity .ai-step-image {
|
|
898
|
+
display: flex;
|
|
899
|
+
width: 12rem;
|
|
900
|
+
max-width: 100%;
|
|
901
|
+
height: 8rem;
|
|
902
|
+
flex-shrink: 0;
|
|
903
|
+
align-items: center;
|
|
904
|
+
justify-content: center;
|
|
905
|
+
overflow: hidden;
|
|
906
|
+
padding: 0;
|
|
907
|
+
border-radius: var(--k2b-radius-control);
|
|
908
|
+
background: var(--ui-surface-muted);
|
|
909
|
+
box-shadow: var(--ui-control-recess);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
.k2b-ui .k2b-chat-activity .ai-step-image img {
|
|
913
|
+
width: 100%;
|
|
914
|
+
height: 100%;
|
|
915
|
+
object-fit: cover;
|
|
916
|
+
object-position: top;
|
|
917
|
+
}
|
|
@@ -167,3 +167,28 @@
|
|
|
167
167
|
gap: 0.5rem;
|
|
168
168
|
margin-left: auto;
|
|
169
169
|
}
|
|
170
|
+
|
|
171
|
+
/* A saved file's progress, or the field for another name when its name is taken. Every row keeps the field's
|
|
172
|
+
height, so a conflict never moves the rows below. */
|
|
173
|
+
.cloud-file-saver__slot {
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
min-height: 2.25rem;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.cloud-file-saver__slot > .k2b-progress {
|
|
180
|
+
flex: 1 1 auto;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.cloud-file-saver__rename {
|
|
184
|
+
display: flex;
|
|
185
|
+
flex: 1 1 auto;
|
|
186
|
+
align-items: center;
|
|
187
|
+
gap: 0.5rem;
|
|
188
|
+
min-width: 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.cloud-file-saver__rename > :first-child {
|
|
192
|
+
flex: 1 1 auto;
|
|
193
|
+
min-width: 0;
|
|
194
|
+
}
|
|
@@ -771,6 +771,47 @@
|
|
|
771
771
|
box-shadow: none;
|
|
772
772
|
}
|
|
773
773
|
|
|
774
|
+
/* An app's count on its icon in the rail and the app grid. It is laid over the
|
|
775
|
+
icon's corner and never takes layout space, so a count appearing, growing to
|
|
776
|
+
"99+", or clearing moves nothing. The fill is the solid action colour, the
|
|
777
|
+
same as the sidebar's mention count. */
|
|
778
|
+
.cloud-app-badge {
|
|
779
|
+
position: absolute;
|
|
780
|
+
top: 0;
|
|
781
|
+
right: -0.25rem;
|
|
782
|
+
box-sizing: border-box;
|
|
783
|
+
min-width: 0.875rem;
|
|
784
|
+
height: 0.875rem;
|
|
785
|
+
padding-inline: 0.1875rem;
|
|
786
|
+
border-radius: 999px;
|
|
787
|
+
color: #ffffff;
|
|
788
|
+
background: var(--k2b-action-solid);
|
|
789
|
+
font-size: 0.625rem;
|
|
790
|
+
font-variant-numeric: tabular-nums;
|
|
791
|
+
font-weight: 600;
|
|
792
|
+
line-height: 0.875rem;
|
|
793
|
+
text-align: center;
|
|
794
|
+
white-space: nowrap;
|
|
795
|
+
pointer-events: none;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.app-icon > .cloud-app-badge {
|
|
799
|
+
top: -0.375rem;
|
|
800
|
+
right: -0.375rem;
|
|
801
|
+
min-width: 1.25rem;
|
|
802
|
+
height: 1.25rem;
|
|
803
|
+
padding-inline: 0.3125rem;
|
|
804
|
+
font-size: 0.6875rem;
|
|
805
|
+
line-height: 1.25rem;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
@media (forced-colors: active) {
|
|
809
|
+
.cloud-app-badge {
|
|
810
|
+
outline: 1px solid CanvasText;
|
|
811
|
+
outline-offset: -1px;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
774
815
|
.rail-item-active:hover {
|
|
775
816
|
color: color-mix(in srgb, var(--app-accent, var(--color-blue-500)) 88%, black);
|
|
776
817
|
background: transparent;
|