@k2b/cloud 0.6.0-rc.0 → 0.7.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/README.md +2 -2
- package/package.json +30 -17
- package/scripts/browser-performance.ts +14 -0
- package/scripts/build-pdf-renderer.ts +39 -0
- package/scripts/build.ts +15 -35
- package/scripts/preload.ts +9 -12
- package/scripts/sync-recovery-smoke.ts +2 -1
- package/src/_internal/app-presentation.ts +14 -2
- package/src/_internal/capabilities.ts +108 -7
- package/src/_internal/capability-streams.ts +81 -0
- package/src/_internal/define-app.ts +62 -36
- package/src/_internal/heartbeat.ts +4 -0
- package/src/_internal/help-catalog.ts +27 -172
- package/src/_internal/help.ts +13 -76
- package/src/_internal/page-responses.ts +2 -0
- package/src/_internal/postgres-application-name.ts +30 -0
- package/src/_internal/process-identity.ts +15 -0
- package/src/_internal/process-sync.ts +1 -1
- package/src/_internal/registry-validation.ts +29 -25
- package/src/_internal/registry.ts +2 -53
- package/src/_internal/runtime-context.ts +5 -16
- package/src/_internal/server-timing.ts +13 -0
- package/src/_internal/status-preserving-ssr.ts +18 -6
- package/src/_internal/web-vitals-asset.ts +10 -0
- package/src/access/PermissionEditor.tsx +14 -153
- package/src/access/PrincipalPicker.tsx +104 -0
- package/src/access/ui.ts +2 -0
- package/src/ai/admin.ts +14 -13
- package/src/ai/assistant-models.ts +12 -1
- package/src/ai/audio-format.ts +34 -0
- package/src/ai/audio-tool.ts +90 -0
- package/src/ai/browser-code-contracts.ts +79 -0
- package/src/ai/browser.ts +2 -0
- package/src/ai/capabilities.ts +55 -36
- package/src/ai/capability-execution.ts +47 -12
- package/src/ai/chat/blocks.tsx +157 -95
- package/src/ai/chat/capability-result.ts +15 -0
- package/src/ai/chat/capability-table.tsx +2 -10
- package/src/ai/chat/composer-adapter.ts +48 -3
- package/src/ai/chat/message-actions.tsx +8 -7
- package/src/ai/chat/message-utils.ts +12 -1
- package/src/ai/chat/messages.ts +2 -0
- package/src/ai/chat/presentation.tsx +19 -48
- package/src/ai/chat/primitives.tsx +43 -1
- package/src/ai/chat/tool-disclosure.tsx +17 -2
- package/src/ai/chat/tool-groups.ts +52 -0
- package/src/ai/chat-quotas.ts +25 -0
- package/src/ai/chat-task-contracts.ts +9 -1
- package/src/ai/chat-tasks.ts +276 -43
- package/src/ai/client/controller.ts +110 -33
- package/src/ai/client/file-source.ts +12 -6
- package/src/ai/client/live-connection.ts +1 -0
- package/src/ai/client/projection.ts +22 -11
- package/src/ai/client/transport.ts +4 -0
- package/src/ai/code-capability-routes.ts +110 -0
- package/src/ai/code-capability-transport.ts +38 -0
- package/src/ai/code-mode-skill.ts +114 -0
- package/src/ai/code-runtime-tools.ts +166 -0
- package/src/ai/code-source-contracts.ts +202 -0
- package/src/ai/code-source-tools.ts +145 -0
- package/src/ai/compaction.ts +27 -6
- package/src/ai/data-analysis-skill.ts +10 -0
- package/src/ai/default-tools.ts +25 -2
- package/src/ai/dictation-runtime.ts +171 -0
- package/src/ai/dictations.ts +188 -0
- package/src/ai/draft-content.ts +34 -0
- package/src/ai/enrich.ts +6 -0
- package/src/ai/executor.ts +191 -31
- package/src/ai/file-content-version.ts +22 -0
- package/src/ai/file-context.ts +2 -1
- package/src/ai/file-media-type.ts +12 -0
- package/src/ai/file-reference-contracts.ts +15 -0
- package/src/ai/file-tools.ts +2 -1
- package/src/ai/files-store.ts +174 -71
- package/src/ai/fixtures/vision-encrypted.pdf +0 -0
- package/src/ai/grids-skill.ts +40 -10
- package/src/ai/http.ts +15 -8
- package/src/ai/index.ts +26 -2
- package/src/ai/inference-calls.ts +198 -0
- package/src/ai/live-events.ts +5 -1
- package/src/ai/memory-learning.ts +60 -28
- package/src/ai/message-queue.ts +167 -0
- package/src/ai/migrate.ts +187 -11
- package/src/ai/model-pricing.ts +43 -0
- package/src/ai/pdf-render-worker.ts +74 -0
- package/src/ai/pdf-render.fixture.ts +28 -0
- package/src/ai/pdf-render.ts +77 -0
- package/src/ai/projects-routes.ts +3 -3
- package/src/ai/projects.ts +20 -6
- package/src/ai/protocol.ts +3 -0
- package/src/ai/provider.ts +1 -8
- package/src/ai/quota-provider.ts +191 -0
- package/src/ai/quota-report.ts +152 -0
- package/src/ai/quotas-migrate.ts +45 -0
- package/src/ai/quotas.ts +168 -0
- package/src/ai/routes.ts +215 -11
- package/src/ai/run-timeout.ts +10 -0
- package/src/ai/runtime-tools.ts +3 -0
- package/src/ai/runtime.ts +55 -16
- package/src/ai/settings.ts +36 -8
- package/src/ai/short-id.ts +2 -2
- package/src/ai/skill-catalog.ts +0 -10
- package/src/ai/skill-search.ts +47 -0
- package/src/ai/skill-seeds.ts +105 -20
- package/src/ai/skill-tool.ts +16 -5
- package/src/ai/skills-routes.ts +32 -4
- package/src/ai/skills.ts +324 -155
- package/src/ai/store.ts +373 -191
- package/src/ai/stream.ts +5 -0
- package/src/ai/structured-runs.ts +4 -3
- package/src/ai/structured.ts +8 -1
- package/src/ai/system-prompt.ts +12 -2
- package/src/ai/timeline.ts +2 -5
- package/src/ai/todo-contracts.ts +20 -0
- package/src/ai/todo-tool.ts +16 -0
- package/src/ai/tool-audit.ts +30 -40
- package/src/ai/tools.ts +6 -0
- package/src/ai/transcription.ts +202 -0
- package/src/ai/turn-timing.ts +114 -0
- package/src/ai/types.ts +58 -10
- package/src/ai/ui.tsx +2 -0
- package/src/ai/usage.ts +118 -118
- package/src/ai/vision-tool.ts +39 -10
- package/src/api/admin-ai-projects.ts +2 -2
- package/src/api/admin-ai-quotas.ts +74 -0
- package/src/api/admin-ai-skills.ts +48 -5
- package/src/api/admin-ai-usage.ts +2 -2
- package/src/api/admin-core-settings.ts +25 -0
- package/src/api/admin-identity.ts +91 -51
- package/src/api/admin-rail.ts +58 -0
- package/src/api/announcements.ts +14 -0
- package/src/api/capabilities.ts +292 -43
- package/src/api/capability-streams.ts +262 -0
- package/src/api/help.ts +8 -44
- package/src/api/index.ts +6 -0
- package/src/api/mcp.ts +44 -26
- package/src/api/me-web-vitals.ts +35 -0
- package/src/api/me.ts +2 -0
- package/src/api/search/schemas.ts +26 -15
- package/src/api/search.bench.ts +1 -1
- package/src/api/search.ts +54 -17
- package/src/browser/CloudResourceSearch.tsx +640 -702
- package/src/browser/app-approval-vault.ts +19 -7
- package/src/browser/command-bridge.ts +109 -0
- package/src/browser/command-shortcuts.ts +76 -0
- package/src/browser/commands.ts +66 -0
- package/src/browser/navigation-search.ts +48 -0
- package/src/browser/notifications.ts +1 -1
- package/src/browser/resource-picker.tsx +5 -18
- package/src/browser/resource-search-dialog.ts +9 -0
- package/src/browser/resource-search-input.ts +46 -0
- package/src/browser/resource-search-messages.ts +76 -40
- package/src/browser/resource-search.ts +7 -0
- package/src/browser/search-bridge.ts +102 -0
- package/src/browser/search-commands.ts +59 -0
- package/src/browser/search.ts +13 -0
- package/src/browser/spotlight-position.ts +208 -0
- package/src/browser/testing.ts +5 -0
- package/src/browser/web-vitals.ts +25 -0
- package/src/capabilities/claims.ts +179 -0
- package/src/capabilities/client.ts +34 -0
- package/src/capabilities/command-link.ts +16 -0
- package/src/capabilities/executions.ts +298 -0
- package/src/capabilities/migrate.ts +81 -0
- package/src/capabilities/server.ts +114 -34
- package/src/capabilities/stream-body.ts +16 -0
- package/src/capabilities/streams.ts +28 -0
- package/src/cli/admin/ai-quotas.ts +161 -0
- package/src/cli/admin/ai-skills.ts +64 -0
- package/src/cli/admin/ai-usage.ts +53 -30
- package/src/cli/admin/app-credentials.ts +79 -0
- package/src/cli/admin/data.ts +17 -0
- package/src/cli/admin/index.ts +9 -0
- package/src/cli/capabilities.ts +29 -0
- package/src/cli/capability-streams.ts +25 -0
- package/src/cli/index.ts +3 -2
- package/src/config/env.ts +11 -8
- package/src/contracts/app.ts +23 -3
- package/src/contracts/capabilities.ts +68 -5
- package/src/contracts/capability-streams.ts +46 -0
- package/src/contracts/commands.ts +49 -0
- package/src/contracts/index.ts +6 -1
- package/src/contracts/rail-admin.ts +38 -0
- package/src/contracts/rail-preferences.ts +16 -0
- package/src/contracts/registry.ts +3 -27
- package/src/contracts/settings-types.ts +1 -1
- package/src/contracts/shared.ts +9 -5
- package/src/contracts/web-vitals.ts +27 -0
- package/src/desktop/solid.tsx +3 -3
- package/src/index.ts +0 -4
- package/src/server/index.ts +3 -0
- package/src/server/middleware/auth.ts +3 -2
- package/src/server/middleware/route-template.ts +9 -3
- package/src/server/middleware/runtime.ts +2 -1
- package/src/server/middleware/settings.ts +17 -11
- package/src/server/services/access-revision.ts +10 -0
- package/src/server/services/access.ts +1 -1
- package/src/server/services/index.ts +1 -0
- package/src/services/account-category-policy.ts +9 -0
- package/src/services/account-lifecycle/audit.ts +1 -1
- package/src/services/accounts/app.ts +12 -2
- package/src/services/accounts/email-write.ts +23 -0
- package/src/services/accounts/groups.ts +15 -4
- package/src/services/accounts/identities.ts +249 -0
- package/src/services/accounts/identity-reconciliation.ts +87 -0
- package/src/services/accounts/ipa-data.ts +2 -2
- package/src/services/accounts/local-groups.ts +2 -2
- package/src/services/accounts/posix.ts +45 -29
- package/src/services/accounts/users.ts +2 -2
- package/src/services/announcements/index.ts +69 -8
- package/src/services/audit/index.ts +1 -1
- package/src/services/cache-fill.ts +35 -0
- package/src/services/help/index.ts +109 -0
- package/src/services/help/maintenance.ts +25 -0
- package/src/services/help/store.ts +180 -0
- package/src/services/help/types.ts +46 -0
- package/src/services/identity/invocation-actor.ts +2 -2
- package/src/services/identity/key-config.ts +2 -1
- package/src/services/identity/key-ring.ts +1 -1
- package/src/services/index.ts +15 -0
- package/src/services/ipa/users.ts +268 -221
- package/src/services/legal-consent.ts +1 -1
- package/src/services/logging/index.ts +15 -13
- package/src/services/logging/trace.ts +2 -1
- package/src/services/logging/web-vitals.ts +81 -0
- package/src/services/mandates/index.ts +18 -4
- package/src/services/mandates/policy.ts +65 -2
- package/src/services/notifications/batches.ts +124 -57
- package/src/services/notifications/browser.ts +12 -0
- package/src/services/notifications/index.ts +0 -2
- package/src/services/notifications/platform.ts +0 -4
- package/src/services/oauth-tokens.ts +3 -3
- package/src/services/pdf/gotenberg.ts +107 -84
- package/src/services/pdf/index.ts +3 -0
- package/src/services/providers/local/users.ts +25 -16
- package/src/services/public-http.ts +101 -0
- package/src/services/rail-shortcuts.ts +95 -0
- package/src/services/rail-snapshot.ts +53 -0
- package/src/services/request-cache-redis.ts +28 -0
- package/src/services/session/index.ts +15 -2
- package/src/services/session/user.ts +62 -37
- package/src/services/settings/core-settings.ts +21 -28
- package/src/services/settings/defaults.ts +5 -0
- package/src/services/settings/store.ts +58 -84
- package/src/shared/ai-costs.ts +42 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/ai-quotas.ts +136 -0
- package/src/shared/ai-usage.ts +6 -16
- package/src/shared/app-presentation.ts +21 -15
- package/src/shared/capability-messages.ts +14 -0
- package/src/shared/help.ts +6 -42
- package/src/shared/index.ts +6 -0
- package/src/ssr/AdminLayout.tsx +7 -5
- package/src/ssr/AdminSidebar.tsx +29 -15
- package/src/ssr/AppLaunchpad.island.tsx +24 -137
- package/src/ssr/AppLaunchpadPanel.tsx +181 -0
- package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
- package/src/ssr/GlobalSearchDialog.tsx +167 -20
- package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
- package/src/ssr/Layout.tsx +17 -6
- package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +46 -14
- package/src/ssr/LayoutHelp.tsx +23 -22
- package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
- package/src/ssr/LayoutRail.tsx +17 -9
- package/src/ssr/MobileNavigation.tsx +71 -0
- package/src/ssr/MobileProfileActions.tsx +49 -0
- package/src/ssr/ProfilePreferences.island.tsx +3 -36
- package/src/ssr/RailApps.island.tsx +2 -2
- package/src/ssr/RailEditor.tsx +17 -2
- package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
- package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
- package/src/ssr/admin-navigation.ts +7 -1
- package/src/ssr/help.ts +29 -0
- package/src/ssr/islands/SearchBar.tsx +67 -0
- package/src/ssr/islands/index.ts +3 -1
- package/src/ssr/layout-context.ts +2 -2
- package/src/ssr/layout-help-search.ts +15 -0
- package/src/ssr/mobile-menu-history.ts +70 -0
- package/src/ssr/platform-messages.ts +16 -8
- package/src/ssr/profile-actions.ts +42 -0
- package/src/ssr/rail-context.ts +8 -4
- package/src/ssr/rail-messages.ts +2 -0
- package/src/ssr/rail-navigation.ts +3 -3
- package/src/ssr/workspace-navigation.ts +75 -0
- package/src/styles/effects.css +6 -6
- package/src/styles/global.css +54 -0
- package/src/styles/input.css +43 -41
- package/src/styles/resource-search.css +505 -0
- package/src/styles/utilities-feedback.css +13 -0
- package/src/styles/utilities-layout.css +1 -5
- package/src/styles/utilities-navigation.css +3 -39
- package/src/types/ambient.d.ts +5 -0
- package/src/workflows/ai/runtime.ts +5 -1
- package/src/workflows/ai/store.ts +2 -0
- package/src/workflows/ai-actions.ts +16 -13
- package/src/workflows/definition.ts +10 -5
- package/src/workflows/store/actions.ts +44 -20
- package/src/workflows/store/index.ts +2 -0
- package/src/workflows/store/runs.ts +36 -0
- package/src/workflows/store/worker-pool.ts +55 -0
- package/src/workflows/store/worker-runtime.ts +90 -0
- package/scripts/app-favicon.test.ts +0 -49
- package/src/ai/chat/blocks.render.test.tsx +0 -1108
- package/src/ai/kit-skill.ts +0 -22
- package/src/ai/usage-migrate.ts +0 -81
- package/src/browser/CloudResourceSearch.behavior.test.tsx +0 -74
- package/src/browser/notification-target.ts +0 -27
- package/src/contracts/notification-live.ts +0 -66
- package/src/services/notifications/live.ts +0 -50
- package/src/ssr/BrowserNotifications.island.tsx +0 -135
- package/src/ssr/GlobalSearchDialog.behavior.test.tsx +0 -96
- package/src/ssr/Layout.render.test.tsx +0 -148
- package/src/ssr/islands/SearchBar.island.tsx +0 -80
|
@@ -28,11 +28,13 @@ const pinSchema = z
|
|
|
28
28
|
const passkeySchema = z
|
|
29
29
|
.object({ type: z.literal("passkey"), credentialId: z.string().min(1).max(2048), salt: base64(32), wrapped: blobSchema })
|
|
30
30
|
.strict();
|
|
31
|
-
const
|
|
31
|
+
const localSchema = z.object({ type: z.literal("local"), key: base64(32) }).strict();
|
|
32
|
+
const methodSchema = z.discriminatedUnion("type", [pinSchema, passkeySchema, localSchema]);
|
|
32
33
|
const configSchema = z
|
|
33
34
|
.object({ version: z.literal(1), id: base64(16), methods: z.array(methodSchema).min(1).max(2) })
|
|
34
35
|
.strict()
|
|
35
|
-
.refine((c) => new Set(c.methods.map((m) => m.type)).size === c.methods.length)
|
|
36
|
+
.refine((c) => new Set(c.methods.map((m) => m.type)).size === c.methods.length)
|
|
37
|
+
.refine((c) => !c.methods.some((m) => m.type === "local") || c.methods.length === 1);
|
|
36
38
|
export type AppVaultConfig = z.infer<typeof configSchema>;
|
|
37
39
|
export type AppVaultBlob = z.infer<typeof blobSchema>;
|
|
38
40
|
export type AppVaultMethod = AppVaultConfig["methods"][number];
|
|
@@ -89,7 +91,7 @@ async function pinKey(pin: string, salt: string) {
|
|
|
89
91
|
function wrapContext(id: string, method: Omit<z.infer<typeof pinSchema>, "wrapped"> | Omit<z.infer<typeof passkeySchema>, "wrapped">) {
|
|
90
92
|
return JSON.stringify(["cloud-login-vault", 1, id, method.type, method.salt, method.type === "pin" ? method.kdf : method.credentialId]);
|
|
91
93
|
}
|
|
92
|
-
function methodContext(id: string, method: AppVaultMethod) {
|
|
94
|
+
function methodContext(id: string, method: Exclude<AppVaultMethod, { type: "local" }>) {
|
|
93
95
|
return wrapContext(
|
|
94
96
|
id,
|
|
95
97
|
method.type === "pin"
|
|
@@ -238,6 +240,11 @@ function session(id: string, raw: Uint8Array<ArrayBuffer>, key: CryptoKey) {
|
|
|
238
240
|
plain.fill(0);
|
|
239
241
|
}
|
|
240
242
|
},
|
|
243
|
+
/** Persists the vault key without PIN protection. Never combine with protected methods. */
|
|
244
|
+
local(): AppVaultMethod {
|
|
245
|
+
check();
|
|
246
|
+
return { type: "local", key: encode(raw) };
|
|
247
|
+
},
|
|
241
248
|
async pin(pin: string): Promise<AppVaultMethod> {
|
|
242
249
|
check();
|
|
243
250
|
const method = { type: "pin" as const, salt: encode(bytes(16)), kdf: "argon2id-64m-t3-p1" as const };
|
|
@@ -281,13 +288,18 @@ async function create() {
|
|
|
281
288
|
const raw = bytes(32);
|
|
282
289
|
return session(encode(bytes(16)), raw, await aes(raw));
|
|
283
290
|
}
|
|
284
|
-
async function unlock(config: AppVaultConfig, type: "
|
|
291
|
+
async function unlock(config: AppVaultConfig, type: AppVaultMethod["type"], pin?: string, signal?: AbortSignal) {
|
|
285
292
|
const value = configSchema.parse(config);
|
|
286
293
|
const method = value.methods.find((m) => m.type === type);
|
|
287
294
|
if (!method) throw invalid();
|
|
288
|
-
const
|
|
289
|
-
method.type === "
|
|
290
|
-
|
|
295
|
+
const raw =
|
|
296
|
+
method.type === "local"
|
|
297
|
+
? decode(method.key)
|
|
298
|
+
: await decrypt(
|
|
299
|
+
method.type === "pin" ? await pinKey(pin ?? "", method.salt) : await passkeyKey(method.credentialId, method.salt, signal),
|
|
300
|
+
method.wrapped,
|
|
301
|
+
methodContext(value.id, method),
|
|
302
|
+
);
|
|
291
303
|
if (raw.length !== 32) {
|
|
292
304
|
raw.fill(0);
|
|
293
305
|
throw invalid();
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { GlobalSearchOptions } from "./search-bridge";
|
|
2
|
+
import type { z } from "zod";
|
|
3
|
+
import type { CommandOptions } from "../contracts/commands";
|
|
4
|
+
|
|
5
|
+
export type CommandTarget = { command: string; input: unknown; options?: CommandOptions };
|
|
6
|
+
export type ContextAwareCommand = {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
description: string;
|
|
10
|
+
icon?: string;
|
|
11
|
+
/** Selection actions appear before page actions. Defaults to page. */
|
|
12
|
+
scope?: "selection" | "page";
|
|
13
|
+
/** One platform-aware key combination, for example mod+shift+k. */
|
|
14
|
+
shortcut?: string;
|
|
15
|
+
/** A linkable app Command, an in-place search, or a local UI interaction. */
|
|
16
|
+
action: CommandTarget | { search: GlobalSearchOptions } | (() => void | Promise<void>);
|
|
17
|
+
};
|
|
18
|
+
type HandleRequest = { target: CommandTarget; handlers: Array<() => void | Promise<void>> };
|
|
19
|
+
type CollectRequest = { commands: ContextAwareCommand[] };
|
|
20
|
+
const HANDLE = "cloud.command.handle";
|
|
21
|
+
const COLLECT = "cloud.command.collect";
|
|
22
|
+
const EXECUTE = "cloud.command.execute";
|
|
23
|
+
type ExecuteRequest = { command: ContextAwareCommand; run: () => Promise<void>; result?: Promise<void> };
|
|
24
|
+
export const COMMANDS_CHANGED = "cloud.command.changed";
|
|
25
|
+
|
|
26
|
+
/** Document events work across independently hydrated islands and bundles. */
|
|
27
|
+
export const registerCommandHandler = <S extends z.ZodType>(
|
|
28
|
+
command: string,
|
|
29
|
+
schema: S,
|
|
30
|
+
handler: (input: z.output<S>, options: CommandOptions) => void | Promise<void>,
|
|
31
|
+
accepts: (input: z.output<S>) => boolean = () => true,
|
|
32
|
+
): (() => void) => {
|
|
33
|
+
let active = true;
|
|
34
|
+
const listener = (event: Event) => {
|
|
35
|
+
const request = (event as CustomEvent<HandleRequest>).detail;
|
|
36
|
+
if (request.target.command !== command) return;
|
|
37
|
+
const parsed = schema.safeParse(request.target.input);
|
|
38
|
+
if (!parsed.success || !accepts(parsed.data)) return;
|
|
39
|
+
// Collect owners before invoking one, so overlapping islands cannot choose by listener order.
|
|
40
|
+
request.handlers.push(() => {
|
|
41
|
+
if (!active) throw new Error("Command context is no longer available");
|
|
42
|
+
return handler(parsed.data, request.target.options ?? {});
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
window.addEventListener(HANDLE, listener);
|
|
46
|
+
return () => {
|
|
47
|
+
active = false;
|
|
48
|
+
window.removeEventListener(HANDLE, listener);
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
export const requestCommandHandling = (target: CommandTarget): Promise<void> | undefined => {
|
|
52
|
+
const detail: HandleRequest = { target, handlers: [] };
|
|
53
|
+
window.dispatchEvent(new CustomEvent(HANDLE, { detail }));
|
|
54
|
+
if (detail.handlers.length > 1) return Promise.reject(new Error("Multiple Command owners accepted the request"));
|
|
55
|
+
const handler = detail.handlers[0];
|
|
56
|
+
return handler ? Promise.resolve().then(handler) : undefined;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Register within a reactive effect and dispose it when the visible context changes. */
|
|
60
|
+
export const registerContextAwareCommand = (command: ContextAwareCommand): (() => void) => {
|
|
61
|
+
let active = true;
|
|
62
|
+
let pending: Promise<void> | undefined;
|
|
63
|
+
const registered = { ...command };
|
|
64
|
+
const execute = (event: Event) => {
|
|
65
|
+
const request = (event as CustomEvent<ExecuteRequest>).detail;
|
|
66
|
+
if (request.command.id !== registered.id || request.command.action !== registered.action) return;
|
|
67
|
+
if (pending) {
|
|
68
|
+
request.result = Promise.resolve();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
pending = Promise.resolve()
|
|
72
|
+
.then(() => {
|
|
73
|
+
if (!active) throw new Error("Command context is no longer available");
|
|
74
|
+
return request.run();
|
|
75
|
+
})
|
|
76
|
+
.finally(() => {
|
|
77
|
+
pending = undefined;
|
|
78
|
+
});
|
|
79
|
+
request.result = pending;
|
|
80
|
+
};
|
|
81
|
+
const listener = (event: Event) => {
|
|
82
|
+
const request = (event as CustomEvent<CollectRequest>).detail;
|
|
83
|
+
request.commands.push(registered);
|
|
84
|
+
};
|
|
85
|
+
window.addEventListener(COLLECT, listener);
|
|
86
|
+
window.addEventListener(EXECUTE, execute);
|
|
87
|
+
window.dispatchEvent(new Event(COMMANDS_CHANGED));
|
|
88
|
+
return () => {
|
|
89
|
+
active = false;
|
|
90
|
+
window.removeEventListener(COLLECT, listener);
|
|
91
|
+
window.removeEventListener(EXECUTE, execute);
|
|
92
|
+
window.dispatchEvent(new Event(COMMANDS_CHANGED));
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
export const collectContextAwareCommands = (): ContextAwareCommand[] => {
|
|
96
|
+
if (typeof window === "undefined") return [];
|
|
97
|
+
const detail: CollectRequest = { commands: [] };
|
|
98
|
+
window.dispatchEvent(new CustomEvent(COLLECT, { detail }));
|
|
99
|
+
// Ambiguous IDs are not executable, regardless of island hydration order.
|
|
100
|
+
return detail.commands
|
|
101
|
+
.filter((command) => detail.commands.filter((other) => other.id === command.id).length === 1)
|
|
102
|
+
.sort((a, b) => Number(b.scope === "selection") - Number(a.scope === "selection"));
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const requestContextCommandExecution = (command: ContextAwareCommand, run: () => Promise<void>): Promise<void> | undefined => {
|
|
106
|
+
const detail: ExecuteRequest = { command, run };
|
|
107
|
+
window.dispatchEvent(new CustomEvent(EXECUTE, { detail }));
|
|
108
|
+
return detail.result;
|
|
109
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { COMMANDS_CHANGED, collectContextAwareCommands, type ContextAwareCommand } from "./command-bridge";
|
|
2
|
+
|
|
3
|
+
const isMac = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform);
|
|
4
|
+
const modifiers = ["ctrl", "alt", "shift", "meta"] as const;
|
|
5
|
+
type Combination = { key: string; ctrl: boolean; alt: boolean; shift: boolean; meta: boolean };
|
|
6
|
+
|
|
7
|
+
const parse = (shortcut: string): Combination | undefined => {
|
|
8
|
+
const parts = shortcut.toLowerCase().split("+");
|
|
9
|
+
const key = parts.pop();
|
|
10
|
+
if (!key || parts.some((part) => !["mod", ...modifiers].includes(part))) return;
|
|
11
|
+
const keys = new Set(parts.map((part) => (part === "mod" ? (isMac() ? "meta" : "ctrl") : part)));
|
|
12
|
+
return { key, ctrl: keys.has("ctrl"), alt: keys.has("alt"), shift: keys.has("shift"), meta: keys.has("meta") };
|
|
13
|
+
};
|
|
14
|
+
const signature = (shortcut: string) => {
|
|
15
|
+
const parsed = parse(shortcut);
|
|
16
|
+
return parsed && JSON.stringify(parsed);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Shared projection for the dispatcher, palette and Help. Conflicts never choose a winner. */
|
|
20
|
+
export const contextCommandsWithShortcuts = (): ContextAwareCommand[] => {
|
|
21
|
+
const commands = collectContextAwareCommands();
|
|
22
|
+
const keys = commands.map((command) => (command.shortcut ? signature(command.shortcut) : undefined));
|
|
23
|
+
return commands.map((command, index) => ({
|
|
24
|
+
...command,
|
|
25
|
+
shortcut: keys[index] && keys.filter((key) => key === keys[index]).length === 1 ? command.shortcut : undefined,
|
|
26
|
+
}));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const shortcutLabel = (shortcut: string): string => {
|
|
30
|
+
const combination = parse(shortcut);
|
|
31
|
+
if (!combination) return "";
|
|
32
|
+
const mac = isMac();
|
|
33
|
+
const names = { ctrl: mac ? "⌃" : "Ctrl", alt: mac ? "⌥" : "Alt", shift: mac ? "⇧" : "Shift", meta: mac ? "⌘" : "Meta" };
|
|
34
|
+
return [...modifiers.filter((key) => combination[key]).map((key) => names[key]), combination.key.toUpperCase()].join(mac ? "" : "+");
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** One owner in the Cloud layout; app islands only publish commands. */
|
|
38
|
+
export const attachCommandShortcuts = (run: (command: ContextAwareCommand) => void): (() => void) => {
|
|
39
|
+
const listener = (event: KeyboardEvent) => {
|
|
40
|
+
if (event.defaultPrevented || event.repeat || event.isComposing || (!isMac() && event.getModifierState("AltGraph"))) return;
|
|
41
|
+
const target = event.target;
|
|
42
|
+
const editing =
|
|
43
|
+
target instanceof Element &&
|
|
44
|
+
Boolean(target.closest("input, textarea, select, [contenteditable]:not([contenteditable=false]), [role=textbox]"));
|
|
45
|
+
const dialogOpen = Boolean(document.querySelector('dialog[open], [role="dialog"][aria-modal="true"]'));
|
|
46
|
+
const matches = contextCommandsWithShortcuts().filter((command) => {
|
|
47
|
+
if (!command.shortcut) return false;
|
|
48
|
+
if (command.id !== "cloud.search" && dialogOpen) return false;
|
|
49
|
+
const combo = parse(command.shortcut);
|
|
50
|
+
if (!combo || (editing && !combo.ctrl && !combo.meta)) return false;
|
|
51
|
+
// Shift+/ reports '?' on common keyboard layouts.
|
|
52
|
+
const key = event.key === "?" && combo.key === "/" ? "/" : event.key.toLowerCase();
|
|
53
|
+
const physicalLetter =
|
|
54
|
+
combo.alt &&
|
|
55
|
+
(event.key === "Dead" || !/^[a-z]$/i.test(event.key)) &&
|
|
56
|
+
/^[a-z]$/.test(combo.key) &&
|
|
57
|
+
event.code === `Key${combo.key.toUpperCase()}`;
|
|
58
|
+
return (
|
|
59
|
+
(key === combo.key || physicalLetter) &&
|
|
60
|
+
event.ctrlKey === combo.ctrl &&
|
|
61
|
+
event.altKey === combo.alt &&
|
|
62
|
+
event.shiftKey === combo.shift &&
|
|
63
|
+
event.metaKey === combo.meta
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
if (matches.length !== 1) return;
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
event.stopPropagation();
|
|
69
|
+
run(matches[0]!);
|
|
70
|
+
};
|
|
71
|
+
// Registered app combinations must win over editor bindings such as Mod+Shift+K.
|
|
72
|
+
window.addEventListener("keydown", listener, true);
|
|
73
|
+
return () => window.removeEventListener("keydown", listener, true);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export { COMMANDS_CHANGED };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { requestGlobalSearch } from "./search-bridge";
|
|
2
|
+
import { toast } from "@k2b/ui";
|
|
3
|
+
import { resolveCommand } from "../capabilities/client";
|
|
4
|
+
import { clearCommand, CommandOptionsSchema, readCommand, type CommandOptions } from "../contracts/commands";
|
|
5
|
+
import { resourceSearchMessages } from "./resource-search-messages";
|
|
6
|
+
import {
|
|
7
|
+
collectContextAwareCommands,
|
|
8
|
+
requestContextCommandExecution,
|
|
9
|
+
type ContextAwareCommand,
|
|
10
|
+
requestCommandHandling,
|
|
11
|
+
} from "./command-bridge";
|
|
12
|
+
export { registerCommandHandler, registerContextAwareCommand } from "./command-bridge";
|
|
13
|
+
export type { CommandTarget, ContextAwareCommand } from "./command-bridge";
|
|
14
|
+
export type { CommandOptions } from "../contracts/commands";
|
|
15
|
+
|
|
16
|
+
/** Opens locally only when a mounted owner explicitly accepts; otherwise follows the app's URL. */
|
|
17
|
+
export const openCommand = async (command: string, input: unknown = {}, options: CommandOptions = {}): Promise<void> => {
|
|
18
|
+
const parsedOptions = CommandOptionsSchema.parse(options);
|
|
19
|
+
const handled = requestCommandHandling({ command, input, options: parsedOptions });
|
|
20
|
+
if (handled) return handled;
|
|
21
|
+
const result = await resolveCommand(command, input, parsedOptions);
|
|
22
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
23
|
+
window.location.assign(result.data.href);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Call after this page's handlers have mounted. Each entry is consumed, including invalid links. */
|
|
27
|
+
export const consumeCommandLink = async (): Promise<void> => {
|
|
28
|
+
const url = new URL(window.location.href);
|
|
29
|
+
const messages = resourceSearchMessages.resolve([document.documentElement.lang]).t;
|
|
30
|
+
let target: ReturnType<typeof readCommand>;
|
|
31
|
+
try {
|
|
32
|
+
target = readCommand(url);
|
|
33
|
+
} catch {
|
|
34
|
+
window.history.replaceState(window.history.state, "", clearCommand(url));
|
|
35
|
+
toast.error(messages.commandFailed);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!target) return;
|
|
39
|
+
window.history.replaceState(window.history.state, "", clearCommand(url));
|
|
40
|
+
try {
|
|
41
|
+
const handled = requestCommandHandling({ command: target.id, input: target.input, options: target.options });
|
|
42
|
+
if (!handled) {
|
|
43
|
+
toast.error(messages.commandUnavailable);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
await handled;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
toast.error(error instanceof Error && error.message ? error.message : messages.commandFailed);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** The palette and keyboard share live ownership checks, pending guards and error feedback. */
|
|
53
|
+
export const runContextAwareCommand = async (command: ContextAwareCommand): Promise<void> => {
|
|
54
|
+
const messages = resourceSearchMessages.resolve([document.documentElement.lang]).t;
|
|
55
|
+
try {
|
|
56
|
+
const current = collectContextAwareCommands().find((item) => item.id === command.id && item.action === command.action);
|
|
57
|
+
if (!current) throw new Error(messages.commandUnavailable);
|
|
58
|
+
await requestContextCommandExecution(current, async () => {
|
|
59
|
+
if (typeof current.action === "function") await current.action();
|
|
60
|
+
else if ("search" in current.action) await requestGlobalSearch({ query: "", ...current.action.search });
|
|
61
|
+
else await openCommand(current.action.command, current.action.input, current.action.options);
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
toast.error(error instanceof Error && error.message ? error.message : messages.commandFailed);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { fuzzy } from "@k2b/stdlib";
|
|
2
|
+
import type { SearchItem } from "../api/search/schemas";
|
|
3
|
+
import type { AppMeta } from "../contracts/app";
|
|
4
|
+
|
|
5
|
+
/** UI-only rows. The synthetic ref is a row key, never a readable resource. */
|
|
6
|
+
export type NavigationSearchItem = SearchItem & { keywords?: readonly string[] };
|
|
7
|
+
|
|
8
|
+
export const navigationSearchItems = (apps: readonly AppMeta[], visibleAppIds: readonly string[]): NavigationSearchItem[] => {
|
|
9
|
+
const visible = new Set(visibleAppIds);
|
|
10
|
+
return apps
|
|
11
|
+
.filter((app) => visible.has(app.id))
|
|
12
|
+
.flatMap((app) =>
|
|
13
|
+
(app.searchLinks ?? []).map((link) => ({
|
|
14
|
+
appId: app.id,
|
|
15
|
+
appName: app.name,
|
|
16
|
+
appIcon: app.icon,
|
|
17
|
+
ref: { type: "cloud.navigation", id: `${app.id}:${link.href}` },
|
|
18
|
+
readable: false,
|
|
19
|
+
title: link.label,
|
|
20
|
+
preview: link.description,
|
|
21
|
+
href: link.href,
|
|
22
|
+
icon: link.icon ?? app.icon,
|
|
23
|
+
priority: 0,
|
|
24
|
+
keywords: link.keywords,
|
|
25
|
+
})),
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const matchNavigationSearchItems = (
|
|
30
|
+
items: readonly NavigationSearchItem[],
|
|
31
|
+
input: { query: string; tags: readonly string[]; appId?: string | null; requireReader?: boolean },
|
|
32
|
+
): NavigationSearchItem[] => {
|
|
33
|
+
const query = input.query.trim().toLowerCase();
|
|
34
|
+
if (input.requireReader || input.tags.length > 0 || (!input.appId && query.length < 2)) return [];
|
|
35
|
+
if (input.appId) {
|
|
36
|
+
const scoped = items.filter((item) => item.appId === input.appId);
|
|
37
|
+
return query
|
|
38
|
+
? fuzzy
|
|
39
|
+
.filter(query, scoped, { key: (item) => [item.title, item.preview ?? "", ...(item.keywords ?? [])].join(" ") })
|
|
40
|
+
.map((hit) => hit.item)
|
|
41
|
+
: scoped;
|
|
42
|
+
}
|
|
43
|
+
const words = query.split(/\s+/);
|
|
44
|
+
return items.filter((item) => {
|
|
45
|
+
const text = [item.title, item.preview ?? "", ...(item.keywords ?? [])].join(" ").toLowerCase();
|
|
46
|
+
return words.every((word) => text.includes(word));
|
|
47
|
+
});
|
|
48
|
+
};
|
|
@@ -56,7 +56,7 @@ const subscriptionPayload = (subscription: PushSubscription): BrowserPushSubscri
|
|
|
56
56
|
};
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
const responseError = async (response: Response, fallback: string): Promise<Error> => {
|
|
59
|
+
const responseError = async (response: Pick<Response, "json">, fallback: string): Promise<Error> => {
|
|
60
60
|
try {
|
|
61
61
|
const payload = (await response.json()) as { message?: unknown };
|
|
62
62
|
if (typeof payload.message === "string" && payload.message.trim()) return new Error(payload.message);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { dialogCore } from "@k2b/ui";
|
|
2
2
|
import type { SearchItem } from "../api/search/schemas";
|
|
3
3
|
import type { CloudResourceRef } from "../contracts";
|
|
4
4
|
import CloudResourceSearch from "./CloudResourceSearch";
|
|
5
|
+
import { resourceSearchDialogOptions } from "./resource-search-dialog";
|
|
5
6
|
import { resourceSearchMessages } from "./resource-search-messages";
|
|
6
7
|
|
|
7
8
|
export type CloudResourcePickerItem = SearchItem;
|
|
8
|
-
|
|
9
9
|
export type CloudResourcePickerOptions = {
|
|
10
10
|
title?: string;
|
|
11
11
|
placeholder?: string;
|
|
@@ -17,23 +17,10 @@ export type CloudResourcePickerOptions = {
|
|
|
17
17
|
export const openCloudResourcePicker = (options: CloudResourcePickerOptions = {}): Promise<CloudResourcePickerItem | undefined> => {
|
|
18
18
|
const locale = typeof document === "undefined" ? "en" : document.documentElement.lang || "en";
|
|
19
19
|
const t = resourceSearchMessages.resolve([locale]).t;
|
|
20
|
-
return
|
|
20
|
+
return dialogCore.open<CloudResourcePickerItem>(
|
|
21
21
|
(close) => (
|
|
22
|
-
<
|
|
23
|
-
<CloudResourceSearch
|
|
24
|
-
initialAppId={options.initialAppId}
|
|
25
|
-
excludeRefs={options.excludeRefs}
|
|
26
|
-
requireReader={options.requireReader}
|
|
27
|
-
placeholder={options.placeholder ?? t.searchPlaceholder}
|
|
28
|
-
onSelect={close}
|
|
29
|
-
/>
|
|
30
|
-
</div>
|
|
22
|
+
<CloudResourceSearch {...options} title={options.title ?? t.chooseResource} selectionMode onSelect={close} onClose={() => close()} />
|
|
31
23
|
),
|
|
32
|
-
|
|
33
|
-
title: options.title ?? t.chooseResource,
|
|
34
|
-
icon: "ti ti-cloud-search",
|
|
35
|
-
size: "large",
|
|
36
|
-
ariaLabel: options.title ?? t.chooseResource,
|
|
37
|
-
},
|
|
24
|
+
resourceSearchDialogOptions(options.title ?? t.chooseResource),
|
|
38
25
|
);
|
|
39
26
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { OpenDialogOptions } from "@k2b/ui";
|
|
2
|
+
|
|
3
|
+
/** One shell for global navigation and resource selection, with native focus/escape handling. */
|
|
4
|
+
export const resourceSearchDialogOptions = (ariaLabel: string): OpenDialogOptions => ({
|
|
5
|
+
panelClassName: "cloud-search-dialog",
|
|
6
|
+
contentClassName: "cloud-search-dialog__content",
|
|
7
|
+
initialFocus: "first-input",
|
|
8
|
+
ariaLabel,
|
|
9
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { SearchApp } from "../api/search/schemas";
|
|
2
|
+
|
|
3
|
+
export type SearchTag = NonNullable<SearchApp["tags"]>[number] & { appName: string; appIcon: string };
|
|
4
|
+
|
|
5
|
+
/** Keep aliases discoverable without listing the same facet repeatedly. */
|
|
6
|
+
export const searchTags = (apps: readonly SearchApp[], appId?: string | null): SearchTag[] => {
|
|
7
|
+
const seen = new Set<string>();
|
|
8
|
+
return apps
|
|
9
|
+
.filter((app) => !appId || app.id === appId)
|
|
10
|
+
.flatMap((app) =>
|
|
11
|
+
(app.tags ?? []).flatMap((tag) => {
|
|
12
|
+
if (seen.has(tag.tag)) return [];
|
|
13
|
+
seen.add(tag.tag);
|
|
14
|
+
return [{ ...tag, appName: app.name, appIcon: app.icon }];
|
|
15
|
+
}),
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const tagAtCursor = (text: string, caret: number) => {
|
|
20
|
+
const pos = Math.min(caret, text.length);
|
|
21
|
+
let start = pos;
|
|
22
|
+
let end = pos;
|
|
23
|
+
while (start > 0 && !/\s/.test(text[start - 1]!)) start--;
|
|
24
|
+
while (end < text.length && !/\s/.test(text[end]!)) end++;
|
|
25
|
+
if (text[start] !== "#" || pos <= start || text.slice(start + 1, end).includes("#")) return null;
|
|
26
|
+
return { start, end, prefix: text.slice(start + 1, pos).toLowerCase() };
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Only whitespace commits a typed/pasted tag; an unfinished #prefix is not an error. */
|
|
30
|
+
export const commitTypedTags = (text: string, caret: number) => {
|
|
31
|
+
const tags: string[] = [];
|
|
32
|
+
let nextCaret = caret;
|
|
33
|
+
const input = text.replace(/(^|\s)#([^\s#]+)(?=\s)/g, (match: string, space: string, tag: string, offset: number) => {
|
|
34
|
+
tags.push(tag.toLowerCase());
|
|
35
|
+
if (offset < caret) nextCaret -= Math.min(match.length - space.length, caret - offset - space.length);
|
|
36
|
+
return space;
|
|
37
|
+
});
|
|
38
|
+
return { input, caret: Math.max(0, nextCaret), tags };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const matchingSearchTags = (tags: readonly SearchTag[], prefix: string, selected: readonly string[]) =>
|
|
42
|
+
tags.filter(
|
|
43
|
+
(tag) =>
|
|
44
|
+
!selected.some((value) => value === tag.tag || tag.aliases?.includes(value)) &&
|
|
45
|
+
(!prefix || [tag.tag, tag.title.toLowerCase(), ...(tag.aliases ?? [])].some((value) => value.startsWith(prefix))),
|
|
46
|
+
);
|
|
@@ -4,52 +4,88 @@ export const resourceSearchMessages = i18n.define({
|
|
|
4
4
|
baseLocale: "en",
|
|
5
5
|
messages: {
|
|
6
6
|
en: {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
actions: "Actions",
|
|
8
|
+
contextActions: "On this page",
|
|
9
|
+
commandPlaceholder: "Find an action…",
|
|
10
|
+
commandFailed: "Could not open this action. Try again.",
|
|
11
|
+
commandUnavailable: "This action is no longer available. Choose another action.",
|
|
12
|
+
commandHint: "Choose an action, or type > next time.",
|
|
13
|
+
searchFailed: "Search is currently unavailable. Try again.",
|
|
14
|
+
navigationFailed: "Could not open this result. Try again.",
|
|
15
|
+
partialFailure: "Some search sources are unavailable. Results may be incomplete.",
|
|
16
|
+
searchPlaceholder: "Search Cloud…",
|
|
17
|
+
scopedSearchPlaceholder: "Search…",
|
|
9
18
|
searchCloudResources: "Search Cloud resources",
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
chooseResource: "Choose a resource",
|
|
20
|
+
pickerPlaceholder: "Find a resource…",
|
|
21
|
+
close: "Close search",
|
|
22
|
+
add: "Add",
|
|
23
|
+
chooseFirst: "Select a resource",
|
|
24
|
+
details: "Details",
|
|
25
|
+
back: "Back to results",
|
|
26
|
+
tagSuggestions: "Search filters",
|
|
27
|
+
allTags: "All filters",
|
|
28
|
+
backToSearch: "Back to search",
|
|
29
|
+
navigationHint: "Search available pages.",
|
|
30
|
+
startHint: "Find content, or narrow your search with a tag.",
|
|
31
|
+
pickerHint: "Find a note, file, or another resource.",
|
|
32
|
+
noMatches: "No matches. Try another search term.",
|
|
33
|
+
noTags: "No matching filter.",
|
|
34
|
+
unsupportedTags: "Unknown filter. Remove it or choose another.",
|
|
35
|
+
removeTag: ({ tag }: { tag: string }) => `Remove #${tag}`,
|
|
36
|
+
removeScope: ({ app }: { app: string }) => `Search beyond ${app}`,
|
|
15
37
|
resultCount: ({ count }: { count: number }) => i18n.plural(count, "en", { one: `${count} result`, other: `${count} results` }),
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
noMatches: "No matches",
|
|
26
|
-
matchesFor: "for",
|
|
27
|
-
selectPreview: "Select a result to preview details.",
|
|
28
|
-
chooseResource: "Choose Cloud resource",
|
|
38
|
+
retry: "Try again",
|
|
39
|
+
navigate: "navigate",
|
|
40
|
+
open: "open",
|
|
41
|
+
newTab: "new tab",
|
|
42
|
+
homeHint: "Back to center",
|
|
43
|
+
releaseHint: "Release to center",
|
|
44
|
+
select: "select",
|
|
45
|
+
loading: "Searching…",
|
|
46
|
+
loadingTags: "Loading filters…",
|
|
29
47
|
},
|
|
30
48
|
de: {
|
|
31
|
-
|
|
32
|
-
|
|
49
|
+
actions: "Aktionen",
|
|
50
|
+
contextActions: "Auf dieser Seite",
|
|
51
|
+
commandPlaceholder: "Aktion finden…",
|
|
52
|
+
commandFailed: "Die Aktion konnte nicht geöffnet werden. Versuche es erneut.",
|
|
53
|
+
commandUnavailable: "Diese Aktion ist nicht mehr verfügbar. Wähle eine andere Aktion.",
|
|
54
|
+
commandHint: "Wähle eine Aktion – beim nächsten Mal geht auch >.",
|
|
55
|
+
searchFailed: "Suche gerade nicht erreichbar. Versuche es erneut.",
|
|
56
|
+
navigationFailed: "Der Treffer konnte nicht geöffnet werden. Versuche es erneut.",
|
|
57
|
+
partialFailure: "Einige Suchquellen sind nicht erreichbar. Ergebnisse können unvollständig sein.",
|
|
58
|
+
searchPlaceholder: "Cloud durchsuchen…",
|
|
59
|
+
scopedSearchPlaceholder: "Durchsuchen…",
|
|
33
60
|
searchCloudResources: "Cloud-Inhalte durchsuchen",
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
61
|
+
chooseResource: "Ressource wählen",
|
|
62
|
+
pickerPlaceholder: "Ressource suchen…",
|
|
63
|
+
close: "Suche schließen",
|
|
64
|
+
add: "Hinzufügen",
|
|
65
|
+
chooseFirst: "Eine Ressource auswählen",
|
|
66
|
+
details: "Details",
|
|
67
|
+
back: "Zurück zu Ergebnissen",
|
|
68
|
+
tagSuggestions: "Suchfilter",
|
|
69
|
+
allTags: "Alle Filter",
|
|
70
|
+
backToSearch: "Zurück zur Suche",
|
|
71
|
+
navigationHint: "Verfügbare Seiten durchsuchen.",
|
|
72
|
+
startHint: "Inhalte finden oder mit einem Tag eingrenzen.",
|
|
73
|
+
pickerHint: "Finde eine Notiz, Datei oder einen anderen Inhalt.",
|
|
74
|
+
noMatches: "Keine Treffer. Versuche einen anderen Suchbegriff.",
|
|
75
|
+
noTags: "Kein passender Filter.",
|
|
76
|
+
unsupportedTags: "Unbekannter Filter. Entferne ihn oder wähle einen anderen.",
|
|
77
|
+
removeTag: ({ tag }) => `#${tag} entfernen`,
|
|
78
|
+
removeScope: ({ app }) => `Außerhalb von ${app} suchen`,
|
|
39
79
|
resultCount: ({ count }) => i18n.plural(count, "de", { one: `${count} Ergebnis`, other: `${count} Ergebnisse` }),
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
noMatches: "Keine Treffer",
|
|
50
|
-
matchesFor: "für",
|
|
51
|
-
selectPreview: "Wähle ein Ergebnis aus, um Details anzuzeigen.",
|
|
52
|
-
chooseResource: "Cloud-Inhalt auswählen",
|
|
80
|
+
retry: "Erneut versuchen",
|
|
81
|
+
navigate: "navigieren",
|
|
82
|
+
open: "öffnen",
|
|
83
|
+
newTab: "neuer Tab",
|
|
84
|
+
homeHint: "Zurück zur Mitte",
|
|
85
|
+
releaseHint: "Loslassen zum Zentrieren",
|
|
86
|
+
select: "auswählen",
|
|
87
|
+
loading: "Suche läuft…",
|
|
88
|
+
loadingTags: "Filter werden geladen…",
|
|
53
89
|
},
|
|
54
90
|
},
|
|
55
91
|
});
|
|
@@ -8,12 +8,19 @@ export const cloudResourceSearchUrl = (request: {
|
|
|
8
8
|
query: string;
|
|
9
9
|
tags: readonly string[];
|
|
10
10
|
appId?: string | null;
|
|
11
|
+
scope?: CloudResourceRef;
|
|
12
|
+
scopeTag?: string;
|
|
11
13
|
requireReader?: boolean;
|
|
12
14
|
}): string => {
|
|
13
15
|
const params = new URLSearchParams({ provider_limit: String(PROVIDER_LIMIT) });
|
|
14
16
|
if (request.query.length > 0) params.set("q", request.query);
|
|
15
17
|
for (const tag of request.tags) params.append("tag", tag);
|
|
16
18
|
if (request.appId) params.set("app", request.appId);
|
|
19
|
+
if (request.scopeTag !== undefined) params.set("scope_tag", request.scopeTag);
|
|
20
|
+
if (request.scope) {
|
|
21
|
+
params.set("scope_type", request.scope.type);
|
|
22
|
+
params.set("scope_id", request.scope.id);
|
|
23
|
+
}
|
|
17
24
|
if (request.requireReader) params.set("require_reader", "true");
|
|
18
25
|
return `/api/search?${params.toString()}`;
|
|
19
26
|
};
|