@k2b/cloud 0.33.0 → 0.35.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 +3 -3
- package/src/_internal/app-presentation.ts +40 -11
- package/src/_internal/define-app.ts +1 -0
- 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 +130 -16
- package/src/ai/firecrawl-tools.ts +3 -30
- 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/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/network-security.ts +33 -9
- package/src/services/pdf/markdown.ts +11 -7
- package/src/services/pwa-devices.ts +68 -15
- 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/network-address.ts +46 -45
- package/src/ssr/Layout.tsx +3 -15
- package/src/ssr/LayoutHelp.browser-harness.tsx +43 -0
- package/src/ssr/LayoutHelp.tsx +320 -183
- package/src/ssr/LayoutHelpTrigger.tsx +1 -3
- package/src/ssr/LayoutPreferences.tsx +15 -5
- package/src/ssr/MinimalLayoutPreferences.island.tsx +3 -9
- package/src/ssr/MobileProfileActions.tsx +3 -8
- package/src/ssr/ProfilePreferences.tsx +8 -10
- package/src/ssr/ShellRuntime.island.tsx +1 -5
- package/src/ssr/ThemeCycleButton.tsx +46 -0
- package/src/ssr/app-appearance.ts +6 -1
- package/src/ssr/layout-help-url.ts +39 -0
- package/src/ssr/platform-messages.ts +0 -8
- package/src/ssr/preference-controller.ts +53 -19
- package/src/ssr/profile-actions.ts +12 -16
- package/src/ssr/profile-preferences-messages.ts +4 -2
- package/src/ssr/rail-navigation.ts +3 -0
- package/src/ssr/shell-data.tsx +2 -2
- package/src/styles/effects.css +13 -3
- package/src/styles/global.css +47 -12
- package/src/styles/input.css +0 -8
- package/src/styles/tokens.css +0 -2
- package/src/styles/utilities-navigation.css +10 -13
- package/src/ssr/GlobalSearchHelpDialog.tsx +0 -104
|
@@ -2,7 +2,6 @@ import { IconButton, Tooltip, useLocale } from "@k2b/ui";
|
|
|
2
2
|
import { importOnDemand } from "../browser/reload";
|
|
3
3
|
import { createBusyAction } from "./busy-action";
|
|
4
4
|
import { platformMessages } from "./platform-messages";
|
|
5
|
-
import { useShellData } from "./shell-data";
|
|
6
5
|
|
|
7
6
|
type LayoutHelpTriggerProps = {
|
|
8
7
|
variant: "header" | "rail";
|
|
@@ -13,12 +12,11 @@ type LayoutHelpTriggerProps = {
|
|
|
13
12
|
export default function LayoutHelpTrigger(props: LayoutHelpTriggerProps) {
|
|
14
13
|
const locale = useLocale();
|
|
15
14
|
const t = () => platformMessages.resolve([locale()]).t;
|
|
16
|
-
const data = useShellData();
|
|
17
15
|
const action = createBusyAction();
|
|
18
16
|
const openHelp = () =>
|
|
19
17
|
action.run(
|
|
20
18
|
() => importOnDemand(() => import("./LayoutHelp")),
|
|
21
|
-
(help) => help.openLayoutHelpDialog(
|
|
19
|
+
(help) => help.openLayoutHelpDialog(),
|
|
22
20
|
);
|
|
23
21
|
|
|
24
22
|
if (props.variant === "rail")
|
|
@@ -12,10 +12,20 @@ export default function LayoutPreferences(props: LayoutPreferencesProps) {
|
|
|
12
12
|
const preferences = createPreferenceController(props.initialTheme, locale);
|
|
13
13
|
|
|
14
14
|
return (
|
|
15
|
-
|
|
16
|
-
<Dropdown.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
<>
|
|
16
|
+
<Dropdown.Root
|
|
17
|
+
items={preferences.items()}
|
|
18
|
+
label={preferences.messages().preferencesMenuLabel}
|
|
19
|
+
position={props.position}
|
|
20
|
+
width="14rem"
|
|
21
|
+
>
|
|
22
|
+
<Dropdown.Trigger iconOnly label={preferences.messages().preferencesMenuLabel} size="sm" variant="secondary">
|
|
23
|
+
<i class="ti ti-adjustments-horizontal" aria-hidden="true" />
|
|
24
|
+
</Dropdown.Trigger>
|
|
25
|
+
</Dropdown.Root>
|
|
26
|
+
<span class="sr-only" aria-live="polite">
|
|
27
|
+
{preferences.themeAnnouncement()}
|
|
28
|
+
</span>
|
|
29
|
+
</>
|
|
20
30
|
);
|
|
21
31
|
}
|
|
@@ -3,13 +3,13 @@ import { canonicalLocale } from "../shared/locale";
|
|
|
3
3
|
import { type ProfilePreferenceLocale, profilePreferenceLocale, setLocalePreference } from "../shared/locale-preference";
|
|
4
4
|
import type { CloudThemePreference } from "../shared/theme";
|
|
5
5
|
import { createPreferenceController } from "./preference-controller";
|
|
6
|
+
import { ThemeCycleButton } from "./ThemeCycleButton";
|
|
6
7
|
|
|
7
|
-
/**
|
|
8
|
+
/** Language opens a menu; appearance cycles in place and keeps its accessible name below md. */
|
|
8
9
|
export default function MinimalLayoutPreferences(props: { initialTheme: CloudThemePreference }) {
|
|
9
10
|
const locale = useLocale();
|
|
10
11
|
const preferences = createPreferenceController(props.initialTheme, locale);
|
|
11
12
|
const t = preferences.messages;
|
|
12
|
-
const currentTheme = () => preferences.themeOption(preferences.theme());
|
|
13
13
|
const language = () => profilePreferenceLocale(locale());
|
|
14
14
|
const languageName = () => (language() === "de" ? t().switchToGerman : t().switchToEnglish);
|
|
15
15
|
// Keep a regional locale such as en-GB when its language is chosen again,
|
|
@@ -37,13 +37,7 @@ export default function MinimalLayoutPreferences(props: { initialTheme: CloudThe
|
|
|
37
37
|
<i class="ti ti-chevron-down hidden md:inline" aria-hidden="true" />
|
|
38
38
|
</Dropdown.Trigger>
|
|
39
39
|
</Dropdown.Root>
|
|
40
|
-
<
|
|
41
|
-
<Dropdown.Trigger label={`${t().appearance}: ${currentTheme().label}`} size="sm" tooltip={false} variant="ghost">
|
|
42
|
-
<i class={currentTheme().icon} aria-hidden="true" />
|
|
43
|
-
<span class="hidden md:inline">{currentTheme().label}</span>
|
|
44
|
-
<i class="ti ti-chevron-down hidden md:inline" aria-hidden="true" />
|
|
45
|
-
</Dropdown.Trigger>
|
|
46
|
-
</Dropdown.Root>
|
|
40
|
+
<ThemeCycleButton preferences={preferences} size="sm" variant="ghost" compact />
|
|
47
41
|
</div>
|
|
48
42
|
);
|
|
49
43
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { documentNavigate } from "@k2b/ssr/nav";
|
|
2
|
-
import { Button, ButtonLink
|
|
2
|
+
import { Button, ButtonLink } from "@k2b/ui";
|
|
3
3
|
import type { CloudThemePreference } from "../shared/theme";
|
|
4
4
|
import { createProfileActions } from "./profile-actions";
|
|
5
|
+
import { ThemeCycleButton } from "./ThemeCycleButton";
|
|
5
6
|
|
|
6
7
|
export function MobileProfileActions(props: {
|
|
7
8
|
profile: { name: string; theme: CloudThemePreference; appHref?: string };
|
|
@@ -17,13 +18,7 @@ export function MobileProfileActions(props: {
|
|
|
17
18
|
};
|
|
18
19
|
return (
|
|
19
20
|
<section class="cloud-mobile-profile" aria-label={preferences.messages().menuLabel}>
|
|
20
|
-
<
|
|
21
|
-
class="cloud-mobile-profile__appearance"
|
|
22
|
-
ariaLabel={preferences.messages().appearance}
|
|
23
|
-
options={preferences.themeOptions()}
|
|
24
|
-
value={preferences.theme}
|
|
25
|
-
onValueChange={preferences.setTheme}
|
|
26
|
-
/>
|
|
21
|
+
<ThemeCycleButton preferences={preferences} variant="ghost" />
|
|
27
22
|
<ButtonLink href="/me" variant="ghost" onClick={navigate("/me")}>
|
|
28
23
|
<i class="ti ti-user-circle" aria-hidden="true" />
|
|
29
24
|
{preferences.messages().profileSettings}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Avatar, Dropdown
|
|
1
|
+
import { Avatar, Dropdown } from "@k2b/ui";
|
|
2
2
|
import type { CloudThemePreference } from "../shared/theme";
|
|
3
3
|
import { createProfileActions } from "./profile-actions";
|
|
4
4
|
|
|
@@ -26,15 +26,10 @@ export default function ProfilePreferences(props: ProfilePreferencesProps) {
|
|
|
26
26
|
{avatar()}
|
|
27
27
|
</button>
|
|
28
28
|
<div class="layout-profile-preferences__panel dropdown-menu-surface">
|
|
29
|
-
<
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
options={preferences.themeOptions().map(({ value, label }) => ({ value, label }))}
|
|
34
|
-
value={preferences.theme}
|
|
35
|
-
onValueChange={preferences.setTheme}
|
|
36
|
-
/>
|
|
37
|
-
</div>
|
|
29
|
+
<button type="button" class="menu-item" aria-label={preferences.themeCycleLabel()} onClick={preferences.cycleTheme}>
|
|
30
|
+
<i class={preferences.themeOption(preferences.theme()).icon} aria-hidden="true" />
|
|
31
|
+
<span>{preferences.themeOption(preferences.theme()).label}</span>
|
|
32
|
+
</button>
|
|
38
33
|
<button type="button" class="menu-item" onClick={preferences.toggleLanguage}>
|
|
39
34
|
<i class="ti ti-language" aria-hidden="true" />
|
|
40
35
|
<span>{preferences.languageLabel()}</span>
|
|
@@ -71,6 +66,9 @@ export default function ProfilePreferences(props: ProfilePreferencesProps) {
|
|
|
71
66
|
{avatar()}
|
|
72
67
|
</Dropdown.Trigger>
|
|
73
68
|
</Dropdown.Root>
|
|
69
|
+
<span class="sr-only" aria-live="polite">
|
|
70
|
+
{preferences.themeAnnouncement()}
|
|
71
|
+
</span>
|
|
74
72
|
</div>
|
|
75
73
|
);
|
|
76
74
|
}
|
|
@@ -128,11 +128,7 @@ export default function ShellRuntime(props: ShellRuntimeProps) {
|
|
|
128
128
|
icon: "ti ti-help-circle",
|
|
129
129
|
shortcut: "shift+/",
|
|
130
130
|
// Returning the load keeps the Command busy until the dialog is open; a second press opens no second dialog.
|
|
131
|
-
action: () =>
|
|
132
|
-
importOnDemand(() => import("./LayoutHelp")).then((help) => {
|
|
133
|
-
const data = readShellData();
|
|
134
|
-
help?.openLayoutHelpDialog(data?.searchHelpApps ?? [], data?.accent);
|
|
135
|
-
}),
|
|
131
|
+
action: () => importOnDemand(() => import("./LayoutHelp")).then((help) => help?.openLayoutHelpDialog()),
|
|
136
132
|
}),
|
|
137
133
|
),
|
|
138
134
|
);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Button, type ButtonSize, type ButtonVariant } from "@k2b/ui";
|
|
2
|
+
import { type createPreferenceController, THEME_CYCLE } from "./preference-controller";
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
preferences: ReturnType<typeof createPreferenceController>;
|
|
6
|
+
variant: ButtonVariant;
|
|
7
|
+
size?: ButtonSize;
|
|
8
|
+
/** Shows only the icon below `md`; the accessible name still states the choice. */
|
|
9
|
+
compact?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One button for the appearance: it shows the current choice and each click switches to the next. The label reserves
|
|
14
|
+
* the width of the two other choices, so the button and its neighbors stay in place.
|
|
15
|
+
*/
|
|
16
|
+
export function ThemeCycleButton(props: Props) {
|
|
17
|
+
const current = () => props.preferences.themeOption(props.preferences.theme());
|
|
18
|
+
const otherLabels = () =>
|
|
19
|
+
THEME_CYCLE.filter((value) => value !== props.preferences.theme()).map((value) => props.preferences.themeOption(value).label);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
<Button
|
|
24
|
+
type="button"
|
|
25
|
+
class="cloud-theme-cycle"
|
|
26
|
+
variant={props.variant}
|
|
27
|
+
size={props.size}
|
|
28
|
+
aria-label={props.preferences.themeCycleLabel()}
|
|
29
|
+
onClick={props.preferences.cycleTheme}
|
|
30
|
+
>
|
|
31
|
+
<i class={current().icon} aria-hidden="true" />
|
|
32
|
+
<span
|
|
33
|
+
class="cloud-theme-cycle__label"
|
|
34
|
+
data-compact={props.compact ? "true" : undefined}
|
|
35
|
+
data-reserve-a={otherLabels()[0]}
|
|
36
|
+
data-reserve-b={otherLabels()[1]}
|
|
37
|
+
>
|
|
38
|
+
<span>{current().label}</span>
|
|
39
|
+
</span>
|
|
40
|
+
</Button>
|
|
41
|
+
<span class="sr-only" aria-live="polite">
|
|
42
|
+
{props.preferences.themeAnnouncement()}
|
|
43
|
+
</span>
|
|
44
|
+
</>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AppAppearance } from "../contracts/app";
|
|
2
2
|
import { resolveRuntimeRoute } from "./app-navigation";
|
|
3
|
+
import { helpPageAppId } from "./layout-help-url";
|
|
3
4
|
import type { RuntimeContext } from "./runtime";
|
|
4
5
|
|
|
5
6
|
const isHexColor = (value: string | undefined): value is `#${string}` => /^#[0-9a-f]{6}$/i.test(value ?? "");
|
|
@@ -20,4 +21,8 @@ export const appAppearanceStyle = (appearance: AppAppearance | undefined): strin
|
|
|
20
21
|
export const appAccentStyle = (accent: string | undefined): string | undefined =>
|
|
21
22
|
isHexColor(accent) ? `--app-accent:${accent}` : undefined;
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
/** Core's full-page Help of an app belongs to that app. */
|
|
25
|
+
export const resolveCurrentApp = (apps: RuntimeContext["apps"], pathname: string) => {
|
|
26
|
+
const helpAppId = helpPageAppId(pathname);
|
|
27
|
+
return apps.find((app) => app.id === helpAppId) ?? resolveRuntimeRoute(apps, pathname)?.app;
|
|
28
|
+
};
|
|
@@ -3,3 +3,42 @@ export const layoutHelpTopicHref = (pageBase: string, topicId: string | null) =>
|
|
|
3
3
|
const base = pageBase.endsWith("/") ? pageBase.slice(0, -1) : pageBase;
|
|
4
4
|
return topicId ? `${base}/${encodeURIComponent(topicId)}` : base;
|
|
5
5
|
};
|
|
6
|
+
|
|
7
|
+
/** The app named by Core's exact full-page Help route. */
|
|
8
|
+
export const helpPageAppId = (pathname: string): string | null => {
|
|
9
|
+
const match = /^\/help\/apps\/([^/]+)(?:\/[^/]+)?\/?$/.exec(pathname);
|
|
10
|
+
if (!match?.[1]) return null;
|
|
11
|
+
try {
|
|
12
|
+
return decodeURIComponent(match[1]);
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type HelpLinkTarget = { kind: "section"; hash: string } | { kind: "topic"; topicId: string | null; hash: string };
|
|
19
|
+
|
|
20
|
+
/** Same-app Help links that the reader can open in place. */
|
|
21
|
+
export const helpLinkTarget = (href: string, current: string, help: { appId?: string; pageBase?: string }): HelpLinkTarget | null => {
|
|
22
|
+
try {
|
|
23
|
+
if (href.startsWith("#")) {
|
|
24
|
+
const hash = decodeURIComponent(href.slice(1));
|
|
25
|
+
return hash ? { kind: "section", hash } : null;
|
|
26
|
+
}
|
|
27
|
+
const source = new URL(current);
|
|
28
|
+
const target = new URL(href, source);
|
|
29
|
+
if (target.origin !== source.origin || target.search) return null;
|
|
30
|
+
const bases = [help.pageBase?.replace(/\/$/, ""), help.appId ? `/help/apps/${encodeURIComponent(help.appId)}` : undefined];
|
|
31
|
+
const hash = decodeURIComponent(target.hash.slice(1));
|
|
32
|
+
for (const base of bases) {
|
|
33
|
+
if (!base) continue;
|
|
34
|
+
const path = target.pathname.replace(/\/$/, "");
|
|
35
|
+
if (path === base) return { kind: "topic", topicId: null, hash };
|
|
36
|
+
if (!path.startsWith(`${base}/`)) continue;
|
|
37
|
+
const segment = path.slice(base.length + 1);
|
|
38
|
+
if (segment && !segment.includes("/")) return { kind: "topic", topicId: decodeURIComponent(segment), hash };
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
@@ -56,10 +56,6 @@ export const platformMessages = i18n.define({
|
|
|
56
56
|
latestPlatformUpdates: "Latest platform updates",
|
|
57
57
|
gotIt: "Got it",
|
|
58
58
|
dismissBanner: "Dismiss banner",
|
|
59
|
-
searchTags: "Search tags",
|
|
60
|
-
searchTagsDescription: ({ tag }: { tag: string }) => `Use ${tag} to narrow your search. You can combine text and multiple tags.`,
|
|
61
|
-
tags: "Tags",
|
|
62
|
-
noAppSearchTags: "No app-specific search tags are available.",
|
|
63
59
|
openGlobalSearch: "Open global search",
|
|
64
60
|
globalSearchDescription: "Search across apps, pages, files, and items.",
|
|
65
61
|
searchShortcut: "Search (⌘/Ctrl+K)",
|
|
@@ -127,10 +123,6 @@ export const platformMessages = i18n.define({
|
|
|
127
123
|
latestPlatformUpdates: "Neuigkeiten zur Plattform",
|
|
128
124
|
gotIt: "Verstanden",
|
|
129
125
|
dismissBanner: "Banner ausblenden",
|
|
130
|
-
searchTags: "Such-Tags",
|
|
131
|
-
searchTagsDescription: ({ tag }) => `Verwende ${tag} für eine gezieltere Suche. Text und mehrere Tags lassen sich kombinieren.`,
|
|
132
|
-
tags: "Tags",
|
|
133
|
-
noAppSearchTags: "Keine app-spezifischen Such-Tags verfügbar.",
|
|
134
126
|
openGlobalSearch: "Globale Suche öffnen",
|
|
135
127
|
globalSearchDescription: "Apps, Seiten, Dateien und weitere Inhalte durchsuchen.",
|
|
136
128
|
searchShortcut: "Suchen (⌘/Strg+K)",
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import type { DropdownAction, DropdownItem
|
|
1
|
+
import type { DropdownAction, DropdownItem } from "@k2b/ui";
|
|
2
2
|
import { createMemo, createSignal, onCleanup, onMount } from "solid-js";
|
|
3
3
|
import { profilePreferenceLocale, setLocalePreference } from "../shared/locale-preference";
|
|
4
4
|
import { type CloudThemePreference, getCurrentThemePreference, setThemePreference } from "../shared/theme";
|
|
5
5
|
import { profilePreferencesMessages } from "./profile-preferences-messages";
|
|
6
6
|
|
|
7
7
|
const THEME_PREFERENCE_EVENT = "cloud:theme-preference";
|
|
8
|
-
const
|
|
9
|
-
const THEME_ICONS: Record<CloudThemePreference, string> = { light: "ti ti-sun-high", dark: "ti ti-moon", system: "ti ti-
|
|
8
|
+
export const THEME_CYCLE = ["system", "light", "dark"] as const satisfies readonly CloudThemePreference[];
|
|
9
|
+
const THEME_ICONS: Record<CloudThemePreference, string> = { light: "ti ti-sun-high", dark: "ti ti-moon", system: "ti ti-device-laptop" };
|
|
10
|
+
|
|
11
|
+
export const nextThemePreference = (current: CloudThemePreference): CloudThemePreference =>
|
|
12
|
+
THEME_CYCLE[(THEME_CYCLE.indexOf(current) + 1) % THEME_CYCLE.length]!;
|
|
10
13
|
|
|
11
14
|
export const createPreferenceController = (initialTheme: CloudThemePreference, locale: () => string) => {
|
|
12
15
|
const [theme, updateTheme] = createSignal(initialTheme);
|
|
16
|
+
const [announcedTheme, setAnnouncedTheme] = createSignal<CloudThemePreference>();
|
|
13
17
|
const messages = () => profilePreferencesMessages.resolve([locale()]).t;
|
|
14
18
|
const language = () => profilePreferenceLocale(locale());
|
|
15
19
|
const nextLanguage = () => (language() === "de" ? "en" : "de");
|
|
@@ -22,7 +26,10 @@ export const createPreferenceController = (initialTheme: CloudThemePreference, l
|
|
|
22
26
|
const toggleLanguage = () => setLocalePreference(nextLanguage());
|
|
23
27
|
|
|
24
28
|
onMount(() => {
|
|
25
|
-
const syncTheme = () =>
|
|
29
|
+
const syncTheme = () => {
|
|
30
|
+
const syncedTheme = updateTheme(getCurrentThemePreference());
|
|
31
|
+
if (syncedTheme !== announcedTheme()) setAnnouncedTheme(undefined);
|
|
32
|
+
};
|
|
26
33
|
syncTheme();
|
|
27
34
|
window.addEventListener(THEME_PREFERENCE_EVENT, syncTheme);
|
|
28
35
|
onCleanup(() => window.removeEventListener(THEME_PREFERENCE_EVENT, syncTheme));
|
|
@@ -33,21 +40,48 @@ export const createPreferenceController = (initialTheme: CloudThemePreference, l
|
|
|
33
40
|
icon: THEME_ICONS[value],
|
|
34
41
|
label: { light: messages().themeLight, dark: messages().themeDark, system: messages().themeSystem }[value],
|
|
35
42
|
});
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
const cycleTheme = () => {
|
|
44
|
+
const next = nextThemePreference(theme());
|
|
45
|
+
setTheme(next);
|
|
46
|
+
setAnnouncedTheme(next);
|
|
47
|
+
};
|
|
48
|
+
const themeCycleLabel = () =>
|
|
49
|
+
messages().themeCycle({
|
|
50
|
+
current: themeOption(theme()).label,
|
|
51
|
+
next: themeOption(nextThemePreference(theme())).label,
|
|
52
|
+
});
|
|
53
|
+
const themeAnnouncement = () => {
|
|
54
|
+
const current = announcedTheme();
|
|
55
|
+
return current === undefined ? "" : messages().themeChanged({ current: themeOption(current).label });
|
|
56
|
+
};
|
|
57
|
+
const appearanceItem: DropdownAction = {
|
|
58
|
+
get icon() {
|
|
59
|
+
return themeOption(theme()).icon;
|
|
60
|
+
},
|
|
61
|
+
get label() {
|
|
62
|
+
return themeOption(theme()).label;
|
|
63
|
+
},
|
|
64
|
+
get ariaLabel() {
|
|
65
|
+
return themeCycleLabel();
|
|
66
|
+
},
|
|
67
|
+
closeOnSelect: false,
|
|
68
|
+
action: cycleTheme,
|
|
69
|
+
};
|
|
49
70
|
const languageItem = (): DropdownAction => ({ action: toggleLanguage, icon: "ti ti-language", label: languageLabel() });
|
|
50
|
-
const items = createMemo<DropdownItem[]>(() => [
|
|
71
|
+
const items = createMemo<DropdownItem[]>(() => [appearanceItem, languageItem()]);
|
|
51
72
|
|
|
52
|
-
return {
|
|
73
|
+
return {
|
|
74
|
+
items,
|
|
75
|
+
appearanceItem,
|
|
76
|
+
languageItem,
|
|
77
|
+
themeOption,
|
|
78
|
+
language,
|
|
79
|
+
languageLabel,
|
|
80
|
+
messages,
|
|
81
|
+
theme,
|
|
82
|
+
cycleTheme,
|
|
83
|
+
themeCycleLabel,
|
|
84
|
+
themeAnnouncement,
|
|
85
|
+
toggleLanguage,
|
|
86
|
+
};
|
|
53
87
|
};
|
|
@@ -31,23 +31,19 @@ export function createProfileActions(initialTheme: CloudThemePreference, appHref
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
const items = createMemo<DropdownItem[]>(() => [
|
|
34
|
-
preferences.
|
|
34
|
+
preferences.appearanceItem,
|
|
35
|
+
preferences.languageItem(),
|
|
35
36
|
{
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
icon: "ti ti-logout",
|
|
47
|
-
label: preferences.messages().signOut,
|
|
48
|
-
disabled: signingOut(),
|
|
49
|
-
},
|
|
50
|
-
],
|
|
37
|
+
href: "/me",
|
|
38
|
+
icon: "ti ti-user-circle",
|
|
39
|
+
label: preferences.messages().profileSettings,
|
|
40
|
+
},
|
|
41
|
+
...(appHref ? [{ href: appHref, icon: "ti ti-device-mobile", label: preferences.messages().app }] : []),
|
|
42
|
+
{
|
|
43
|
+
action: signOut,
|
|
44
|
+
icon: "ti ti-logout",
|
|
45
|
+
label: preferences.messages().signOut,
|
|
46
|
+
disabled: signingOut(),
|
|
51
47
|
},
|
|
52
48
|
]);
|
|
53
49
|
|
|
@@ -12,10 +12,11 @@ export const profilePreferencesMessages = i18n.define({
|
|
|
12
12
|
app: "App",
|
|
13
13
|
signOut: "Sign out",
|
|
14
14
|
signOutFailed: "Sign out failed. Please try again.",
|
|
15
|
-
appearance: "Appearance",
|
|
16
15
|
themeLight: "Light",
|
|
17
16
|
themeDark: "Dark",
|
|
18
17
|
themeSystem: "Automatic",
|
|
18
|
+
themeCycle: ({ current, next }: { current: string; next: string }) => `Appearance: ${current}. Switches to ${next}.`,
|
|
19
|
+
themeChanged: ({ current }: { current: string }) => `Appearance: ${current}`,
|
|
19
20
|
switchToGerman: "Deutsch",
|
|
20
21
|
switchToEnglish: "English",
|
|
21
22
|
},
|
|
@@ -28,10 +29,11 @@ export const profilePreferencesMessages = i18n.define({
|
|
|
28
29
|
app: "App",
|
|
29
30
|
signOut: "Abmelden",
|
|
30
31
|
signOutFailed: "Abmelden fehlgeschlagen. Bitte versuche es erneut.",
|
|
31
|
-
appearance: "Darstellung",
|
|
32
32
|
themeLight: "Hell",
|
|
33
33
|
themeDark: "Dunkel",
|
|
34
34
|
themeSystem: "Automatisch",
|
|
35
|
+
themeCycle: ({ current, next }) => `Darstellung: ${current}. Wechselt zu ${next}.`,
|
|
36
|
+
themeChanged: ({ current }) => `Darstellung: ${current}`,
|
|
35
37
|
switchToGerman: "Deutsch",
|
|
36
38
|
switchToEnglish: "English",
|
|
37
39
|
},
|
|
@@ -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;
|
package/src/ssr/shell-data.tsx
CHANGED
|
@@ -3,7 +3,6 @@ import type { AppSearchLink } from "../contracts/app";
|
|
|
3
3
|
import type { RailSnapshot } from "../contracts/rail-preferences";
|
|
4
4
|
import type { CloudThemePreference } from "../shared/theme";
|
|
5
5
|
import type { AppLaunchpadLegalLink } from "./app-launchpad";
|
|
6
|
-
import type { GlobalSearchHelpApp } from "./GlobalSearchHelpDialog";
|
|
7
6
|
import type { RailApp } from "./rail-navigation";
|
|
8
7
|
|
|
9
8
|
export type ShellData = {
|
|
@@ -19,7 +18,8 @@ export type ShellData = {
|
|
|
19
18
|
accent?: string;
|
|
20
19
|
/** The search destinations of each app in `apps`, by app id in registry order; the search host expands them into rows. */
|
|
21
20
|
searchLinks: Record<string, readonly AppSearchLink[]>;
|
|
22
|
-
|
|
21
|
+
/** The current app, for links inside its Help. */
|
|
22
|
+
appId?: string;
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
const ShellDataContext = createContext<() => ShellData>();
|
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
|
@@ -255,8 +255,26 @@ html:has(.k2b-mobile-shell) body {
|
|
|
255
255
|
--color-selection-fg: rgb(239 246 255);
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
@
|
|
259
|
-
|
|
258
|
+
/* @k2b/ui makes motion inside .k2b-ui instant under reduced motion; the
|
|
259
|
+
page owns its view transitions, so the same two sources stop them here. */
|
|
260
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
261
|
+
@view-transition {
|
|
262
|
+
navigation: auto;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
@media (prefers-reduced-motion: reduce) {
|
|
267
|
+
::view-transition-group(*),
|
|
268
|
+
::view-transition-old(*),
|
|
269
|
+
::view-transition-new(*) {
|
|
270
|
+
animation: none !important;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
html[data-k2b-motion="reduced"]::view-transition-group(*),
|
|
275
|
+
html[data-k2b-motion="reduced"]::view-transition-old(*),
|
|
276
|
+
html[data-k2b-motion="reduced"]::view-transition-new(*) {
|
|
277
|
+
animation: none !important;
|
|
260
278
|
}
|
|
261
279
|
|
|
262
280
|
::selection {
|
|
@@ -275,15 +293,6 @@ solid-island {
|
|
|
275
293
|
display: contents;
|
|
276
294
|
}
|
|
277
295
|
|
|
278
|
-
@media (prefers-reduced-motion: reduce) {
|
|
279
|
-
*,
|
|
280
|
-
*::before,
|
|
281
|
-
*::after {
|
|
282
|
-
scroll-behavior: auto !important;
|
|
283
|
-
transition-duration: 0.01ms !important;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
296
|
/* Cloud has three shell modes. Phones, below md, get the header and no rail.
|
|
288
297
|
From md, signed-in people get the rail instead of the header: up to 2xl the
|
|
289
298
|
app area meets the window's top, right and bottom edges, and from 2xl it is
|
|
@@ -303,6 +312,10 @@ solid-island {
|
|
|
303
312
|
so short and long pages keep the same width. Full-page surfaces own their
|
|
304
313
|
inner scrolling. */
|
|
305
314
|
html:has(.cloud-app-canvas:not([data-layout-full-page])) { scrollbar-gutter: stable; }
|
|
315
|
+
/* A page that scrolls its content in a ScrollArea grows with it here, so
|
|
316
|
+
the area does not scroll and reserves no second gutter beside the
|
|
317
|
+
document's. From md, `main` bounds its height and it scrolls. */
|
|
318
|
+
.cloud-app-canvas:not([data-layout-full-page]) .layout-content-main > .k2b-scroll-area { scrollbar-gutter: auto; }
|
|
306
319
|
.cloud-app-canvas > .layout-shell-content { gap: 0; }
|
|
307
320
|
.cloud-app-canvas .layout-header {
|
|
308
321
|
border-radius: 0; border: 0; border-bottom: 1px solid var(--ui-border);
|
|
@@ -316,6 +329,29 @@ solid-island {
|
|
|
316
329
|
|
|
317
330
|
}
|
|
318
331
|
|
|
332
|
+
/* Card: the outermost workspace is the app card, so it gets the card's radius
|
|
333
|
+
and frame also when it is flat. Its sidebar and content round their corners
|
|
334
|
+
with that radius and clip what they show. A workspace that is `main`'s own
|
|
335
|
+
child fills `main` and scrolls inside itself, so `main` neither scrolls nor
|
|
336
|
+
clips: it keeps no scrollbar gutter beside the card, the card keeps the
|
|
337
|
+
shell spacing on every side, and its frame shadow shows in that spacing. A
|
|
338
|
+
workspace nested in other page content leaves `main` the page scroller. For
|
|
339
|
+
signed-in people, the flush range below removes the frame again; it comes
|
|
340
|
+
later at equal specificity. */
|
|
341
|
+
@media (min-width: 768px) {
|
|
342
|
+
.cloud-app-canvas .layout-content-main:has(> .k2b-app-workspace, > :is(solid-island, solid-client) > .k2b-app-workspace) {
|
|
343
|
+
overflow: visible;
|
|
344
|
+
scrollbar-gutter: auto;
|
|
345
|
+
}
|
|
346
|
+
.cloud-app-canvas .layout-content-main .k2b-app-workspace[data-flat="true"]:not(.k2b-app-workspace *) {
|
|
347
|
+
--k2b-workspace-radius: var(--k2b-radius-dialog);
|
|
348
|
+
box-shadow: var(--k2b-shadow-frame);
|
|
349
|
+
}
|
|
350
|
+
.dark .cloud-app-canvas .layout-content-main .k2b-app-workspace[data-flat="true"]:not(.k2b-app-workspace *) {
|
|
351
|
+
border-color: var(--k2b-border);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
319
355
|
/* Flush: the app area is one opaque surface from the rail to the window's
|
|
320
356
|
edges, without padding, radius, shadow, or border. The rail keeps the app
|
|
321
357
|
canvas, so the change of surface alone separates the two; no line does. The
|
|
@@ -388,4 +424,3 @@ solid-island {
|
|
|
388
424
|
}
|
|
389
425
|
.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; }
|
|
390
426
|
.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; }
|
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);
|