@k2b/cloud 0.6.0-rc.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 +98 -0
- package/package.json +128 -0
- package/public/logo.svg +1 -0
- package/scripts/app-favicon.test.ts +49 -0
- package/scripts/app-favicon.ts +51 -0
- package/scripts/build.ts +205 -0
- package/scripts/preload.ts +164 -0
- package/scripts/sync-recovery-smoke.ts +200 -0
- package/src/_internal/app-favicon.ts +2 -0
- package/src/_internal/app-presentation.ts +61 -0
- package/src/_internal/app-workspace-state.ts +24 -0
- package/src/_internal/bounded-json.ts +47 -0
- package/src/_internal/build-metadata.ts +9 -0
- package/src/_internal/capabilities.ts +1118 -0
- package/src/_internal/define-app.ts +801 -0
- package/src/_internal/heartbeat.ts +167 -0
- package/src/_internal/help-catalog.ts +241 -0
- package/src/_internal/help.ts +95 -0
- package/src/_internal/nats-connection.ts +34 -0
- package/src/_internal/page-responses.ts +40 -0
- package/src/_internal/process-sync.ts +117 -0
- package/src/_internal/readiness.ts +3 -0
- package/src/_internal/registry-validation.ts +136 -0
- package/src/_internal/registry-watch.ts +52 -0
- package/src/_internal/registry.ts +338 -0
- package/src/_internal/runtime-compatibility.ts +64 -0
- package/src/_internal/runtime-context.ts +84 -0
- package/src/_internal/runtime-watcher.ts +78 -0
- package/src/_internal/static-assets.ts +86 -0
- package/src/_internal/status-preserving-ssr.ts +49 -0
- package/src/access/PermissionEditor.tsx +486 -0
- package/src/access/ResourceApiKeys.tsx +271 -0
- package/src/access/messages.ts +121 -0
- package/src/access/ui.ts +16 -0
- package/src/account/Avatar.tsx +28 -0
- package/src/account/EntitySearch.tsx +363 -0
- package/src/account/avatar-upload-dialog.tsx +197 -0
- package/src/account/avatar-upload.ts +149 -0
- package/src/account/ui.ts +3 -0
- package/src/ai/admin.ts +47 -0
- package/src/ai/approval-routes.ts +130 -0
- package/src/ai/approvals.ts +139 -0
- package/src/ai/assistant-models.ts +61 -0
- package/src/ai/attachments.ts +40 -0
- package/src/ai/browser.ts +92 -0
- package/src/ai/capabilities.ts +714 -0
- package/src/ai/capability-execution.ts +151 -0
- package/src/ai/chat/blocks.render.test.tsx +1108 -0
- package/src/ai/chat/blocks.tsx +730 -0
- package/src/ai/chat/builtin-tools.tsx +428 -0
- package/src/ai/chat/capability-table.tsx +87 -0
- package/src/ai/chat/composer-adapter.ts +169 -0
- package/src/ai/chat/file-tools.tsx +81 -0
- package/src/ai/chat/message-actions.tsx +370 -0
- package/src/ai/chat/message-utils.ts +479 -0
- package/src/ai/chat/messages.ts +43 -0
- package/src/ai/chat/presentation.tsx +395 -0
- package/src/ai/chat/primitives.tsx +3 -0
- package/src/ai/chat/tool-disclosure.tsx +41 -0
- package/src/ai/chat/turn-navigator-utils.ts +28 -0
- package/src/ai/chat/turn-navigator.tsx +164 -0
- package/src/ai/chat/user-message.tsx +149 -0
- package/src/ai/chat/visual-tools.tsx +435 -0
- package/src/ai/chat/web-tools.tsx +202 -0
- package/src/ai/chat-task-contracts.ts +113 -0
- package/src/ai/chat-tasks.ts +952 -0
- package/src/ai/client/controller.ts +1211 -0
- package/src/ai/client/file-source.ts +95 -0
- package/src/ai/client/live-connection.ts +140 -0
- package/src/ai/client/projection.ts +178 -0
- package/src/ai/client/transport.ts +111 -0
- package/src/ai/compaction.ts +182 -0
- package/src/ai/credentials.ts +124 -0
- package/src/ai/default-tool-contracts.ts +108 -0
- package/src/ai/default-tools.ts +127 -0
- package/src/ai/enrich.ts +280 -0
- package/src/ai/executor.ts +1624 -0
- package/src/ai/fetch-file-tool.ts +313 -0
- package/src/ai/file-context.ts +92 -0
- package/src/ai/file-media-type.ts +28 -0
- package/src/ai/file-mount.ts +16 -0
- package/src/ai/file-tools.ts +566 -0
- package/src/ai/files-store.ts +390 -0
- package/src/ai/firecrawl-tools.ts +305 -0
- package/src/ai/grids-skill.ts +126 -0
- package/src/ai/http.ts +265 -0
- package/src/ai/index.ts +337 -0
- package/src/ai/kit-skill.ts +22 -0
- package/src/ai/limits.ts +5 -0
- package/src/ai/live-events.ts +289 -0
- package/src/ai/live-messages.ts +45 -0
- package/src/ai/live-outbox.ts +102 -0
- package/src/ai/live-routes.ts +412 -0
- package/src/ai/live.ts +2 -0
- package/src/ai/maintenance.ts +302 -0
- package/src/ai/markdown-pdf-tool.ts +95 -0
- package/src/ai/memories.ts +605 -0
- package/src/ai/memory-learning-runs.ts +222 -0
- package/src/ai/memory-learning.ts +619 -0
- package/src/ai/memory-tool.ts +120 -0
- package/src/ai/memory-workflow-evidence.ts +158 -0
- package/src/ai/migrate.ts +2082 -0
- package/src/ai/model-access-migrate.ts +30 -0
- package/src/ai/model-access.ts +191 -0
- package/src/ai/personal-agent.ts +6 -0
- package/src/ai/prefs.ts +72 -0
- package/src/ai/project-tool.ts +94 -0
- package/src/ai/projects-routes.ts +277 -0
- package/src/ai/projects.ts +902 -0
- package/src/ai/protocol.ts +279 -0
- package/src/ai/provider.ts +73 -0
- package/src/ai/public-projection.ts +25 -0
- package/src/ai/resource-markers.ts +31 -0
- package/src/ai/resource-refs.ts +70 -0
- package/src/ai/routes.ts +1131 -0
- package/src/ai/runtime-tools.ts +39 -0
- package/src/ai/runtime.ts +544 -0
- package/src/ai/settings.ts +529 -0
- package/src/ai/short-id.ts +45 -0
- package/src/ai/skill-catalog.ts +73 -0
- package/src/ai/skill-format.ts +145 -0
- package/src/ai/skill-seeds.ts +430 -0
- package/src/ai/skill-tool.ts +81 -0
- package/src/ai/skills-routes.ts +143 -0
- package/src/ai/skills.ts +934 -0
- package/src/ai/solid.ts +27 -0
- package/src/ai/store.ts +3113 -0
- package/src/ai/stream.ts +308 -0
- package/src/ai/structured-runs.ts +66 -0
- package/src/ai/structured.ts +186 -0
- package/src/ai/system-prompt.ts +170 -0
- package/src/ai/task-prompt.ts +22 -0
- package/src/ai/timeline.ts +148 -0
- package/src/ai/tool-audit.ts +177 -0
- package/src/ai/tool-result-budget.ts +16 -0
- package/src/ai/tools.ts +155 -0
- package/src/ai/types.ts +967 -0
- package/src/ai/ui.tsx +39 -0
- package/src/ai/usage-migrate.ts +81 -0
- package/src/ai/usage.ts +315 -0
- package/src/ai/validate.ts +54 -0
- package/src/ai/vision-tool.ts +79 -0
- package/src/api/accounts-entities.ts +174 -0
- package/src/api/admin-ai-projects.ts +74 -0
- package/src/api/admin-ai-skills.ts +76 -0
- package/src/api/admin-ai-usage.ts +42 -0
- package/src/api/admin-core-settings.ts +508 -0
- package/src/api/admin-identity.ts +148 -0
- package/src/api/admin-lifecycle.ts +206 -0
- package/src/api/admin-linux-identities.ts +66 -0
- package/src/api/announcements.ts +131 -0
- package/src/api/app-approval.ts +129 -0
- package/src/api/apps.ts +51 -0
- package/src/api/auth/schemas.ts +54 -0
- package/src/api/auth.ts +370 -0
- package/src/api/capabilities.ts +658 -0
- package/src/api/help-messages.ts +19 -0
- package/src/api/help.ts +78 -0
- package/src/api/index.ts +108 -0
- package/src/api/mcp.ts +744 -0
- package/src/api/me-rail.ts +45 -0
- package/src/api/me.ts +753 -0
- package/src/api/search/schemas.ts +79 -0
- package/src/api/search.bench.ts +185 -0
- package/src/api/search.ts +407 -0
- package/src/api/sync-ops.ts +79 -0
- package/src/api/widgets.ts +143 -0
- package/src/browser/CloudResourceSearch.behavior.test.tsx +74 -0
- package/src/browser/CloudResourceSearch.tsx +791 -0
- package/src/browser/app-approval-vault.ts +305 -0
- package/src/browser/app-approval.ts +234 -0
- package/src/browser/live-websocket.ts +268 -0
- package/src/browser/locale-preference.ts +25 -0
- package/src/browser/notification-target.ts +27 -0
- package/src/browser/notification-timeout.ts +14 -0
- package/src/browser/notifications.ts +181 -0
- package/src/browser/resource-clipboard.ts +70 -0
- package/src/browser/resource-picker.tsx +39 -0
- package/src/browser/resource-search-messages.ts +55 -0
- package/src/browser/resource-search.ts +27 -0
- package/src/capabilities/client.ts +149 -0
- package/src/capabilities/response.ts +39 -0
- package/src/capabilities/server.ts +215 -0
- package/src/capabilities/signals.ts +4 -0
- package/src/capabilities/testing.ts +12 -0
- package/src/capabilities/types.ts +32 -0
- package/src/cli/access.ts +499 -0
- package/src/cli/account.ts +382 -0
- package/src/cli/admin/account-administration.ts +45 -0
- package/src/cli/admin/account-categories.ts +49 -0
- package/src/cli/admin/ai-usage.ts +118 -0
- package/src/cli/admin/app-sign-in.ts +49 -0
- package/src/cli/admin/data.ts +343 -0
- package/src/cli/admin/documentation.ts +34 -0
- package/src/cli/admin/gateway.ts +155 -0
- package/src/cli/admin/index.ts +61 -0
- package/src/cli/admin/instance.ts +250 -0
- package/src/cli/admin/jobs.ts +223 -0
- package/src/cli/admin/legal.ts +122 -0
- package/src/cli/admin/linux.ts +53 -0
- package/src/cli/admin/logs.ts +324 -0
- package/src/cli/admin/metrics.ts +142 -0
- package/src/cli/admin/nats.ts +167 -0
- package/src/cli/admin/notifications.ts +516 -0
- package/src/cli/admin/shared.ts +113 -0
- package/src/cli/admin/sync.ts +188 -0
- package/src/cli/admin/telemetry.ts +314 -0
- package/src/cli/admin/webhooks.ts +222 -0
- package/src/cli/admin/workflows.ts +367 -0
- package/src/cli/apps.ts +40 -0
- package/src/cli/capabilities.ts +239 -0
- package/src/cli/commands.ts +604 -0
- package/src/cli/index.ts +61 -0
- package/src/cli/locale.ts +31 -0
- package/src/cli/output.ts +58 -0
- package/src/clients/core.ts +20 -0
- package/src/config/env.ts +54 -0
- package/src/config/index.ts +6 -0
- package/src/contracts/account-categories.ts +62 -0
- package/src/contracts/announcements.ts +121 -0
- package/src/contracts/app-approval.ts +197 -0
- package/src/contracts/app.ts +162 -0
- package/src/contracts/capabilities.ts +484 -0
- package/src/contracts/index.ts +14 -0
- package/src/contracts/notification-live.ts +66 -0
- package/src/contracts/notification-types.ts +235 -0
- package/src/contracts/posix.ts +69 -0
- package/src/contracts/profile.ts +85 -0
- package/src/contracts/rail-preferences.ts +53 -0
- package/src/contracts/registry.ts +110 -0
- package/src/contracts/resource-clipboard.ts +33 -0
- package/src/contracts/settings-types.ts +98 -0
- package/src/contracts/shared.ts +408 -0
- package/src/contracts/user-notifications.ts +101 -0
- package/src/contracts/widgets.ts +183 -0
- package/src/desktop/index.ts +704 -0
- package/src/desktop/solid.tsx +938 -0
- package/src/index.ts +58 -0
- package/src/server/actor.ts +41 -0
- package/src/server/api/index.ts +2 -0
- package/src/server/api/respond.ts +96 -0
- package/src/server/api-client.ts +54 -0
- package/src/server/app-context.ts +39 -0
- package/src/server/help.ts +187 -0
- package/src/server/index.ts +89 -0
- package/src/server/locale.ts +55 -0
- package/src/server/middleware/auth.ts +386 -0
- package/src/server/middleware/index.ts +28 -0
- package/src/server/middleware/invocation.ts +55 -0
- package/src/server/middleware/middleware.ts +65 -0
- package/src/server/middleware/openapi.ts +126 -0
- package/src/server/middleware/rate-limit.ts +125 -0
- package/src/server/middleware/request-logger.ts +41 -0
- package/src/server/middleware/route-template.ts +46 -0
- package/src/server/middleware/runtime.ts +22 -0
- package/src/server/middleware/settings.ts +55 -0
- package/src/server/middleware/validator.ts +49 -0
- package/src/server/middleware/workload.ts +22 -0
- package/src/server/ratelimit.ts +151 -0
- package/src/server/request-cookies.ts +18 -0
- package/src/server/services/access.ts +644 -0
- package/src/server/services/freeipa/client.ts +137 -0
- package/src/server/services/freeipa/index.ts +9 -0
- package/src/server/services/freeipa/session.ts +123 -0
- package/src/server/services/freeipa/test-certificates.ts +44 -0
- package/src/server/services/freeipa/tls.ts +44 -0
- package/src/server/services/freeipa/transport.ts +116 -0
- package/src/server/services/freeipa/util.ts +59 -0
- package/src/server/services/geo.ts +198 -0
- package/src/server/services/index.ts +48 -0
- package/src/server/services/pagination.ts +22 -0
- package/src/server/services/services.ts +13 -0
- package/src/server/time.ts +33 -0
- package/src/services/account-category-policy.ts +51 -0
- package/src/services/account-lifecycle/audit.ts +41 -0
- package/src/services/account-lifecycle/index.ts +940 -0
- package/src/services/account-lifecycle/ipa-backfill.ts +152 -0
- package/src/services/account-lifecycle/notification-sender.ts +13 -0
- package/src/services/account-lifecycle/scheduler.ts +506 -0
- package/src/services/account-model.ts +22 -0
- package/src/services/accounts/app.ts +1671 -0
- package/src/services/accounts/authz.ts +44 -0
- package/src/services/accounts/avatar.ts +120 -0
- package/src/services/accounts/base-group.ts +11 -0
- package/src/services/accounts/base-user.ts +46 -0
- package/src/services/accounts/duplicate-emails.ts +82 -0
- package/src/services/accounts/entities.ts +622 -0
- package/src/services/accounts/group-sql.ts +106 -0
- package/src/services/accounts/groups.ts +272 -0
- package/src/services/accounts/index.ts +14 -0
- package/src/services/accounts/ipa-data.ts +66 -0
- package/src/services/accounts/lifecycle.ts +2 -0
- package/src/services/accounts/local-groups.ts +499 -0
- package/src/services/accounts/model.ts +129 -0
- package/src/services/accounts/notification-sender.ts +31 -0
- package/src/services/accounts/posix.ts +386 -0
- package/src/services/accounts/request-policy.ts +11 -0
- package/src/services/accounts/switching.ts +126 -0
- package/src/services/accounts/users.ts +745 -0
- package/src/services/announcements/index.ts +224 -0
- package/src/services/app-approval.ts +498 -0
- package/src/services/audit/index.ts +438 -0
- package/src/services/auth-flows/index.ts +15 -0
- package/src/services/auth-flows/ipa.ts +197 -0
- package/src/services/auth-flows/magic-link.ts +185 -0
- package/src/services/auth-flows/notification-sender.ts +10 -0
- package/src/services/auth-flows/password-reset.ts +268 -0
- package/src/services/auth-flows/proxy-return.ts +49 -0
- package/src/services/document-extraction.ts +148 -0
- package/src/services/freeipa-config.ts +179 -0
- package/src/services/gateway.ts +186 -0
- package/src/services/identity/constants.ts +18 -0
- package/src/services/identity/index.ts +52 -0
- package/src/services/identity/invocation-actor.ts +120 -0
- package/src/services/identity/invocation-authority.ts +53 -0
- package/src/services/identity/invocation-operations.ts +15 -0
- package/src/services/identity/invocation-token.ts +276 -0
- package/src/services/identity/key-config.ts +41 -0
- package/src/services/identity/key-ring.ts +530 -0
- package/src/services/identity/metrics.ts +29 -0
- package/src/services/identity/routes.ts +18 -0
- package/src/services/identity/runtime-config.ts +88 -0
- package/src/services/identity/session-token.ts +152 -0
- package/src/services/identity/signer-refresh.ts +44 -0
- package/src/services/identity/workload-auth.ts +64 -0
- package/src/services/index.ts +187 -0
- package/src/services/ipa/auth.ts +216 -0
- package/src/services/ipa/connection.ts +57 -0
- package/src/services/ipa/effective-groups.ts +70 -0
- package/src/services/ipa/groups.ts +722 -0
- package/src/services/ipa/guard.ts +17 -0
- package/src/services/ipa/index.ts +17 -0
- package/src/services/ipa/posix.ts +19 -0
- package/src/services/ipa/profile.ts +132 -0
- package/src/services/ipa/runtime.ts +28 -0
- package/src/services/ipa/search.ts +154 -0
- package/src/services/ipa/service-account.ts +15 -0
- package/src/services/ipa/sync-planning.ts +100 -0
- package/src/services/ipa/sync.ts +862 -0
- package/src/services/ipa/users.ts +806 -0
- package/src/services/legal-consent.ts +76 -0
- package/src/services/logging/index.ts +477 -0
- package/src/services/logging/redaction.ts +20 -0
- package/src/services/logging/trace.ts +1231 -0
- package/src/services/mandates/index.ts +855 -0
- package/src/services/mandates/policy.ts +85 -0
- package/src/services/network-security.ts +31 -0
- package/src/services/notifications/batches.ts +722 -0
- package/src/services/notifications/browser.ts +252 -0
- package/src/services/notifications/catalog.ts +113 -0
- package/src/services/notifications/channels.ts +104 -0
- package/src/services/notifications/dispatcher.ts +139 -0
- package/src/services/notifications/email.ts +118 -0
- package/src/services/notifications/index.ts +633 -0
- package/src/services/notifications/live.ts +50 -0
- package/src/services/notifications/observability.ts +407 -0
- package/src/services/notifications/platform.ts +421 -0
- package/src/services/notifications/runtime.ts +120 -0
- package/src/services/notifications/user.ts +248 -0
- package/src/services/notifications/web-push-transport.ts +135 -0
- package/src/services/oauth-tokens.ts +209 -0
- package/src/services/outbox.ts +187 -0
- package/src/services/pdf/gotenberg.ts +290 -0
- package/src/services/pdf/index.ts +43 -0
- package/src/services/pdf/markdown.ts +209 -0
- package/src/services/pdf/template-preview.ts +122 -0
- package/src/services/postgres.ts +76 -0
- package/src/services/providers/index.ts +27 -0
- package/src/services/providers/local/auth.ts +51 -0
- package/src/services/providers/local/index.ts +4 -0
- package/src/services/providers/local/users.ts +263 -0
- package/src/services/rail-preferences.ts +27 -0
- package/src/services/runtime-lifecycle.ts +191 -0
- package/src/services/secrets.ts +10 -0
- package/src/services/service-account-credentials.ts +891 -0
- package/src/services/service-accounts.ts +184 -0
- package/src/services/session/index.ts +236 -0
- package/src/services/session/test-fixture.ts +18 -0
- package/src/services/session/user.ts +144 -0
- package/src/services/settings/api.ts +61 -0
- package/src/services/settings/app.ts +86 -0
- package/src/services/settings/core-settings.ts +796 -0
- package/src/services/settings/crypto.ts +69 -0
- package/src/services/settings/defaults.ts +374 -0
- package/src/services/settings/index.ts +215 -0
- package/src/services/settings/namespace.ts +9 -0
- package/src/services/settings/snapshot.ts +49 -0
- package/src/services/settings/store.ts +241 -0
- package/src/services/settings/templates.ts +4 -0
- package/src/services/sync-ops.ts +403 -0
- package/src/services/topic-cursor.ts +22 -0
- package/src/services/weather/forecast.ts +323 -0
- package/src/services/weather/geo.ts +119 -0
- package/src/services/weather/index.ts +67 -0
- package/src/services/weather/location.ts +24 -0
- package/src/services/weather/locations.ts +133 -0
- package/src/services/weather/migrate.ts +45 -0
- package/src/services/weather/settings.ts +66 -0
- package/src/services/weather/short-id.ts +57 -0
- package/src/services/weather/types.ts +61 -0
- package/src/services/weather/ui.ts +50 -0
- package/src/services/webauthn.ts +385 -0
- package/src/shared/account-action-notice.ts +70 -0
- package/src/shared/account-display.ts +15 -0
- package/src/shared/account-session.ts +15 -0
- package/src/shared/ai-platform-prompt.ts +134 -0
- package/src/shared/ai-usage.ts +49 -0
- package/src/shared/app-presentation.ts +48 -0
- package/src/shared/app-url.ts +12 -0
- package/src/shared/branding.ts +14 -0
- package/src/shared/capability-messages.ts +70 -0
- package/src/shared/email-html.ts +54 -0
- package/src/shared/format.ts +91 -0
- package/src/shared/help.ts +68 -0
- package/src/shared/icons.ts +400 -0
- package/src/shared/index.ts +54 -0
- package/src/shared/locale.ts +48 -0
- package/src/shared/login-method.ts +44 -0
- package/src/shared/markdown/client.ts +130 -0
- package/src/shared/markdown/extensions/code.ts +105 -0
- package/src/shared/markdown/extensions/guided-help.ts +100 -0
- package/src/shared/markdown/extensions/images.ts +79 -0
- package/src/shared/markdown/extensions/info-blocks.ts +97 -0
- package/src/shared/markdown/extensions/katex.ts +120 -0
- package/src/shared/markdown/extensions/links.ts +43 -0
- package/src/shared/markdown/extensions/mark.ts +48 -0
- package/src/shared/markdown/extensions/sub-sup.ts +60 -0
- package/src/shared/markdown/extensions/tables.ts +109 -0
- package/src/shared/markdown/extensions/task-list.ts +53 -0
- package/src/shared/markdown/formula.ts +1187 -0
- package/src/shared/markdown/index.ts +193 -0
- package/src/shared/markdown/shared.ts +36 -0
- package/src/shared/mock-cover.ts +130 -0
- package/src/shared/network-address.ts +106 -0
- package/src/shared/redirect.ts +56 -0
- package/src/shared/template-rendering.ts +140 -0
- package/src/shared/theme.ts +68 -0
- package/src/shared/time.ts +13 -0
- package/src/ssr/AdminLayout.tsx +53 -0
- package/src/ssr/AdminSidebar.tsx +50 -0
- package/src/ssr/AppLaunchpad.island.tsx +236 -0
- package/src/ssr/AppWorkspaceController.island.tsx +21 -0
- package/src/ssr/BrowserNotifications.island.tsx +135 -0
- package/src/ssr/GlobalAnnouncements.island.tsx +142 -0
- package/src/ssr/GlobalSearchDialog.behavior.test.tsx +96 -0
- package/src/ssr/GlobalSearchDialog.tsx +35 -0
- package/src/ssr/GlobalSearchHelpDialog.tsx +104 -0
- package/src/ssr/GlobalSearchTrigger.island.tsx +55 -0
- package/src/ssr/HotkeysHelpRail.island.tsx +55 -0
- package/src/ssr/Layout.render.test.tsx +148 -0
- package/src/ssr/Layout.tsx +292 -0
- package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
- package/src/ssr/LayoutFooter.tsx +22 -0
- package/src/ssr/LayoutHeader.tsx +81 -0
- package/src/ssr/LayoutHelp.tsx +869 -0
- package/src/ssr/LayoutPreferences.island.tsx +36 -0
- package/src/ssr/LayoutRail.tsx +58 -0
- package/src/ssr/MinimalLayout.tsx +46 -0
- package/src/ssr/PageError.tsx +29 -0
- package/src/ssr/ProfilePreferences.island.tsx +92 -0
- package/src/ssr/RailApps.island.tsx +61 -0
- package/src/ssr/RailEditor.tsx +293 -0
- package/src/ssr/RegisteredHelpDocuments.island.tsx +3 -0
- package/src/ssr/TimezoneCookie.island.tsx +23 -0
- package/src/ssr/admin-active-link.ts +47 -0
- package/src/ssr/admin-navigation.ts +78 -0
- package/src/ssr/app-appearance.ts +23 -0
- package/src/ssr/app-navigation.ts +41 -0
- package/src/ssr/help-messages.ts +104 -0
- package/src/ssr/index.ts +14 -0
- package/src/ssr/islands/SearchBar.island.tsx +80 -0
- package/src/ssr/islands/index.ts +16 -0
- package/src/ssr/layout-context.ts +17 -0
- package/src/ssr/layout-help-markdown.ts +9 -0
- package/src/ssr/layout-help-navigation.ts +24 -0
- package/src/ssr/layout-help-url.ts +5 -0
- package/src/ssr/layout-runtime.ts +22 -0
- package/src/ssr/page-error-messages.ts +25 -0
- package/src/ssr/platform-messages.ts +135 -0
- package/src/ssr/preference-controller.ts +44 -0
- package/src/ssr/profile-preferences-messages.ts +29 -0
- package/src/ssr/rail-context.ts +21 -0
- package/src/ssr/rail-messages.ts +66 -0
- package/src/ssr/rail-navigation.ts +60 -0
- package/src/ssr/runtime.ts +31 -0
- package/src/ssr/url-filter.ts +127 -0
- package/src/styles/base-popover.css +36 -0
- package/src/styles/effects.css +851 -0
- package/src/styles/global.css +268 -0
- package/src/styles/input.css +58 -0
- package/src/styles/tokens.css +230 -0
- package/src/styles/utilities-buttons.css +405 -0
- package/src/styles/utilities-data.css +33 -0
- package/src/styles/utilities-feedback.css +232 -0
- package/src/styles/utilities-layout.css +603 -0
- package/src/styles/utilities-markdown-editor.css +525 -0
- package/src/styles/utilities-markdown-table.css +234 -0
- package/src/styles/utilities-navigation.css +1054 -0
- package/src/types/ambient.d.ts +24 -0
- package/src/workflows/ai/index.ts +17 -0
- package/src/workflows/ai/runtime.ts +286 -0
- package/src/workflows/ai/store.ts +314 -0
- package/src/workflows/ai/types.ts +111 -0
- package/src/workflows/ai-actions.ts +269 -0
- package/src/workflows/authoring.ts +88 -0
- package/src/workflows/builtins.ts +193 -0
- package/src/workflows/contracts.ts +280 -0
- package/src/workflows/definition.ts +334 -0
- package/src/workflows/editor.ts +113 -0
- package/src/workflows/index.ts +71 -0
- package/src/workflows/language/binder.ts +45 -0
- package/src/workflows/language/canonical.ts +29 -0
- package/src/workflows/language/compiler.ts +396 -0
- package/src/workflows/language/expressions.ts +34 -0
- package/src/workflows/language/index.ts +20 -0
- package/src/workflows/language/references.ts +55 -0
- package/src/workflows/language/schema.ts +134 -0
- package/src/workflows/language/strict-yaml.ts +131 -0
- package/src/workflows/manifest.ts +24 -0
- package/src/workflows/module.ts +12 -0
- package/src/workflows/runtime/coordinator.ts +115 -0
- package/src/workflows/runtime/executor.ts +1003 -0
- package/src/workflows/runtime/index.ts +60 -0
- package/src/workflows/runtime/instant.ts +8 -0
- package/src/workflows/runtime/ports.ts +213 -0
- package/src/workflows/runtime/schedule.ts +203 -0
- package/src/workflows/runtime/trigger.ts +37 -0
- package/src/workflows/runtime/wake.ts +13 -0
- package/src/workflows/store/actions.ts +438 -0
- package/src/workflows/store/budget.ts +145 -0
- package/src/workflows/store/definitions.ts +345 -0
- package/src/workflows/store/events.ts +383 -0
- package/src/workflows/store/index.ts +107 -0
- package/src/workflows/store/observability.ts +594 -0
- package/src/workflows/store/runs.ts +975 -0
- package/src/workflows/store/transaction.ts +23 -0
- package/src/workflows/store/worker.ts +439 -0
- package/src/workflows/testing/conformance.ts +72 -0
- package/src/workflows/testing/index.ts +11 -0
- package/src/workflows/testing/process-fixtures.ts +275 -0
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
import { query, timed } from "@k2b/stdlib/solid";
|
|
2
|
+
import { NoticeCard, ScrollArea, Select, useLocale } from "@k2b/ui";
|
|
3
|
+
import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js";
|
|
4
|
+
import type { SearchApp, SearchItem, SearchResponse } from "../api/search/schemas";
|
|
5
|
+
import type { CloudResourceRef } from "../contracts";
|
|
6
|
+
import { cloudResourceSearchUrl, filterCloudResourceSearchItems } from "./resource-search";
|
|
7
|
+
import { resourceSearchMessages } from "./resource-search-messages";
|
|
8
|
+
|
|
9
|
+
type ParsedInput = {
|
|
10
|
+
query: string;
|
|
11
|
+
tags: string[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type CloudResourceSearchHelpApp = {
|
|
15
|
+
appId: string;
|
|
16
|
+
appName: string;
|
|
17
|
+
appIcon: string;
|
|
18
|
+
tags: string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type CloudResourceSearchProps = {
|
|
22
|
+
onSelect: (item: SearchItem) => void;
|
|
23
|
+
onHelp?: () => void;
|
|
24
|
+
helpApps?: readonly CloudResourceSearchHelpApp[];
|
|
25
|
+
initialAppId?: string;
|
|
26
|
+
placeholder?: string;
|
|
27
|
+
requireReader?: boolean;
|
|
28
|
+
excludeRefs?: readonly CloudResourceRef[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type TagSuggestion = {
|
|
32
|
+
tag: string;
|
|
33
|
+
appName: string;
|
|
34
|
+
appIcon: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const MIN_QUERY_LENGTH = 2;
|
|
38
|
+
const SEARCH_DEBOUNCE_MS = 200;
|
|
39
|
+
const TAG_TOKEN_PATTERN = /^[^\s#]+$/;
|
|
40
|
+
|
|
41
|
+
const rowKey = (item: SearchItem) => `row:${item.appId}:${item.ref.type}:${item.ref.id}`;
|
|
42
|
+
const isValidImagePreviewUrl = (url?: string) => typeof url === "string" && url.startsWith("/");
|
|
43
|
+
const sortByPriorityAndTitle = (a: SearchItem, b: SearchItem) => {
|
|
44
|
+
const byPriority = (b.priority ?? 0) - (a.priority ?? 0);
|
|
45
|
+
if (byPriority !== 0) return byPriority;
|
|
46
|
+
return a.title.localeCompare(b.title);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Cluster items by their owning app while preserving the global priority
|
|
51
|
+
* order across groups: the first occurrence of each app fixes its slot, and
|
|
52
|
+
* items within a group keep their original (already sorted) order. Map
|
|
53
|
+
* iteration order in JS is insertion order, so this is stable.
|
|
54
|
+
*/
|
|
55
|
+
const groupByApp = (items: SearchItem[]): SearchItem[] => {
|
|
56
|
+
const groups = new Map<string, SearchItem[]>();
|
|
57
|
+
for (const item of items) {
|
|
58
|
+
const arr = groups.get(item.appId);
|
|
59
|
+
if (arr) arr.push(item);
|
|
60
|
+
else groups.set(item.appId, [item]);
|
|
61
|
+
}
|
|
62
|
+
const out: SearchItem[] = [];
|
|
63
|
+
for (const group of groups.values()) out.push(...group);
|
|
64
|
+
return out;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type ListEntry =
|
|
68
|
+
| { kind: "header"; appId: string; appName: string; appIcon: string; count: number }
|
|
69
|
+
| { kind: "item"; item: SearchItem; flatIndex: number };
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build the flat render list with optional group headers interleaved.
|
|
73
|
+
* Headers only appear when 2+ apps have results (single-app queries stay
|
|
74
|
+
* uncluttered). `flatIndex` on each item entry mirrors the index into the
|
|
75
|
+
* underlying sorted list — used for selection/scroll/keyboard nav.
|
|
76
|
+
*/
|
|
77
|
+
const buildListEntries = (items: SearchItem[]): ListEntry[] => {
|
|
78
|
+
if (items.length === 0) return [];
|
|
79
|
+
|
|
80
|
+
const counts = new Map<string, number>();
|
|
81
|
+
for (const item of items) counts.set(item.appId, (counts.get(item.appId) ?? 0) + 1);
|
|
82
|
+
const showHeaders = counts.size >= 2;
|
|
83
|
+
|
|
84
|
+
const entries: ListEntry[] = [];
|
|
85
|
+
let prevAppId: string | null = null;
|
|
86
|
+
for (let i = 0; i < items.length; i++) {
|
|
87
|
+
const item = items[i]!;
|
|
88
|
+
if (showHeaders && item.appId !== prevAppId) {
|
|
89
|
+
entries.push({
|
|
90
|
+
kind: "header",
|
|
91
|
+
appId: item.appId,
|
|
92
|
+
appName: item.appName,
|
|
93
|
+
appIcon: item.appIcon,
|
|
94
|
+
count: counts.get(item.appId) ?? 0,
|
|
95
|
+
});
|
|
96
|
+
prevAppId = item.appId;
|
|
97
|
+
}
|
|
98
|
+
entries.push({ kind: "item", item, flatIndex: i });
|
|
99
|
+
}
|
|
100
|
+
return entries;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const parseInput = (raw: string): ParsedInput => {
|
|
104
|
+
const tokens = raw.trim().split(/\s+/).filter(Boolean);
|
|
105
|
+
const tags: string[] = [];
|
|
106
|
+
const queryTokens: string[] = [];
|
|
107
|
+
|
|
108
|
+
for (const token of tokens) {
|
|
109
|
+
if (token.startsWith("#") && token.length > 1) {
|
|
110
|
+
const tag = token.slice(1).toLowerCase();
|
|
111
|
+
if (TAG_TOKEN_PATTERN.test(tag)) {
|
|
112
|
+
tags.push(tag);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
queryTokens.push(token);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
query: queryTokens.join(" "),
|
|
122
|
+
tags: [...new Set(tags)],
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
127
|
+
|
|
128
|
+
const removeTagFromInput = (raw: string, tag: string): string => {
|
|
129
|
+
const pattern = new RegExp(`(^|\\s)#${escapeRegex(tag)}(?=\\s|$)`, "gi");
|
|
130
|
+
return raw.replace(pattern, "").replace(/\s+/g, " ").trim();
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const metadataRows = (metadata?: SearchItem["metadata"]) =>
|
|
134
|
+
(metadata ?? []).filter((entry) => entry.label.trim().length > 0 && entry.value.trim().length > 0).slice(0, 5);
|
|
135
|
+
|
|
136
|
+
const loadSearch = async (url: string, abortSignal: AbortSignal, fallbackMessage: string): Promise<SearchResponse> => {
|
|
137
|
+
const response = await fetch(url, { signal: abortSignal });
|
|
138
|
+
const payload = (await response.json()) as SearchResponse | { message?: string };
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
throw new Error("message" in payload ? (payload.message ?? fallbackMessage) : fallbackMessage);
|
|
141
|
+
}
|
|
142
|
+
return payload as SearchResponse;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export default function CloudResourceSearch(props: CloudResourceSearchProps) {
|
|
146
|
+
const locale = useLocale();
|
|
147
|
+
const t = () => resourceSearchMessages.resolve([locale()]).t;
|
|
148
|
+
const [rawInput, setRawInput] = createSignal("");
|
|
149
|
+
const [appId, setAppId] = createSignal<string | null>(props.initialAppId ?? null);
|
|
150
|
+
const [searchUrl, setSearchUrl] = createSignal(
|
|
151
|
+
cloudResourceSearchUrl({ query: "", tags: [], appId: props.initialAppId, requireReader: props.requireReader }),
|
|
152
|
+
);
|
|
153
|
+
const [activeIndex, setActiveIndex] = createSignal(0);
|
|
154
|
+
const [previewFailed, setPreviewFailed] = createSignal(false);
|
|
155
|
+
const [cursorPos, setCursorPos] = createSignal(0);
|
|
156
|
+
const [suggestionIndex, setSuggestionIndex] = createSignal(0);
|
|
157
|
+
|
|
158
|
+
let inputRef: HTMLInputElement | undefined;
|
|
159
|
+
const rowRefs = new Map<string, HTMLButtonElement>();
|
|
160
|
+
|
|
161
|
+
const parsedInput = createMemo(() => parseInput(rawInput()));
|
|
162
|
+
const canSearch = createMemo(() => Boolean(appId()) || parsedInput().tags.length > 0 || parsedInput().query.length >= MIN_QUERY_LENGTH);
|
|
163
|
+
const searchQuery = query.create({
|
|
164
|
+
source: searchUrl,
|
|
165
|
+
enabled: () => searchUrl().length > 0,
|
|
166
|
+
load: (url, { abortSignal }) => loadSearch(url, abortSignal, t().searchFailed),
|
|
167
|
+
});
|
|
168
|
+
const resultItems = createMemo(() => {
|
|
169
|
+
const sorted = filterCloudResourceSearchItems(searchQuery.data()?.items ?? [], props)
|
|
170
|
+
.slice()
|
|
171
|
+
.sort(sortByPriorityAndTitle);
|
|
172
|
+
return groupByApp(sorted);
|
|
173
|
+
});
|
|
174
|
+
const apps = createMemo<SearchApp[]>(() => searchQuery.data()?.apps ?? []);
|
|
175
|
+
const unsupportedTags = createMemo(() => searchQuery.data()?.unsupportedTags ?? []);
|
|
176
|
+
|
|
177
|
+
// Tag suggestions for the empty state — flat list of every tag declared by
|
|
178
|
+
// any app, deduped on tag name (first declaration wins) so we don't render
|
|
179
|
+
// the same chip twice when two apps share a tag.
|
|
180
|
+
const tagSuggestions = createMemo<TagSuggestion[]>(() => {
|
|
181
|
+
const seen = new Set<string>();
|
|
182
|
+
const out: TagSuggestion[] = [];
|
|
183
|
+
for (const app of props.helpApps ?? []) {
|
|
184
|
+
for (const tag of app.tags) {
|
|
185
|
+
const lower = tag.toLowerCase();
|
|
186
|
+
if (seen.has(lower)) continue;
|
|
187
|
+
seen.add(lower);
|
|
188
|
+
out.push({ tag: lower, appName: app.appName, appIcon: app.appIcon });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Detect whether the caret is currently inside a tag token (i.e. the user
|
|
196
|
+
* is mid-typing `#...`). Returns the token's start/end indices in rawInput
|
|
197
|
+
* plus the lowercased prefix typed after `#`. Returns null otherwise — no
|
|
198
|
+
* autocomplete popover should show.
|
|
199
|
+
*
|
|
200
|
+
* Walks back from the cursor to the last whitespace; if the token starts
|
|
201
|
+
* with `#`, we're in tag-input mode. If the prefix contains a non-tag
|
|
202
|
+
* character (`#` or whitespace), bails — the parser would reject it
|
|
203
|
+
* anyway, so suggestions would be misleading.
|
|
204
|
+
*/
|
|
205
|
+
const tagContext = createMemo(() => {
|
|
206
|
+
const text = rawInput();
|
|
207
|
+
const pos = Math.min(cursorPos(), text.length);
|
|
208
|
+
let start = pos;
|
|
209
|
+
while (start > 0 && !/\s/.test(text[start - 1] ?? "")) start--;
|
|
210
|
+
// Extend to the next whitespace so we replace the WHOLE token, not just
|
|
211
|
+
// up to the caret. Otherwise editing in the middle of an existing tag
|
|
212
|
+
// (e.g. caret after `#no` in `#notebook`) leaves `tebook` dangling after
|
|
213
|
+
// the inserted suggestion. Prefix used for matching is still the part
|
|
214
|
+
// BEFORE the caret — that's what the user has committed to typing.
|
|
215
|
+
let end = pos;
|
|
216
|
+
while (end < text.length && !/\s/.test(text[end] ?? "")) end++;
|
|
217
|
+
const fullToken = text.slice(start, end);
|
|
218
|
+
if (!fullToken.startsWith("#")) return null;
|
|
219
|
+
const prefix = text.slice(start + 1, pos).toLowerCase();
|
|
220
|
+
if (prefix.length > 0 && !TAG_TOKEN_PATTERN.test(prefix)) return null;
|
|
221
|
+
return { start, end, prefix };
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Tags filtered by the current prefix, with already-active tags excluded
|
|
226
|
+
* (no point suggesting one the user has). Starts-with match for predictability.
|
|
227
|
+
*/
|
|
228
|
+
const filteredSuggestions = createMemo<TagSuggestion[]>(() => {
|
|
229
|
+
const ctx = tagContext();
|
|
230
|
+
if (!ctx) return [];
|
|
231
|
+
const active = new Set(parsedInput().tags);
|
|
232
|
+
const all = tagSuggestions().filter((s) => !active.has(s.tag));
|
|
233
|
+
if (ctx.prefix.length === 0) return all;
|
|
234
|
+
return all.filter((s) => s.tag.startsWith(ctx.prefix));
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const popoverOpen = createMemo(() => filteredSuggestions().length > 0);
|
|
238
|
+
|
|
239
|
+
// Reset highlight when the filtered list changes shape (new prefix, etc.).
|
|
240
|
+
createEffect(() => {
|
|
241
|
+
filteredSuggestions();
|
|
242
|
+
setSuggestionIndex(0);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const updateCursor = () => {
|
|
246
|
+
if (!inputRef) return;
|
|
247
|
+
setCursorPos(inputRef.selectionStart ?? rawInput().length);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const insertTagAtCursor = (tag: string) => {
|
|
251
|
+
const ctx = tagContext();
|
|
252
|
+
const text = rawInput();
|
|
253
|
+
if (!ctx) {
|
|
254
|
+
// No tag context — append. Used by suggestion-mode chips.
|
|
255
|
+
if (parsedInput().tags.includes(tag)) {
|
|
256
|
+
inputRef?.focus();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const next = text.length > 0 ? `${text.trimEnd()} #${tag} ` : `#${tag} `;
|
|
260
|
+
setRawInput(next);
|
|
261
|
+
const newPos = next.length;
|
|
262
|
+
queueMicrotask(() => {
|
|
263
|
+
if (inputRef) inputRef.setSelectionRange(newPos, newPos);
|
|
264
|
+
setCursorPos(newPos);
|
|
265
|
+
});
|
|
266
|
+
inputRef?.focus();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const before = text.slice(0, ctx.start);
|
|
270
|
+
const after = text.slice(ctx.end).replace(/^\s+/, "");
|
|
271
|
+
// Always emit `#tag ` with one trailing space so the user can keep
|
|
272
|
+
// typing the next token without manual spacing.
|
|
273
|
+
const insertion = `#${tag} `;
|
|
274
|
+
const next = before + insertion + after;
|
|
275
|
+
setRawInput(next);
|
|
276
|
+
const newPos = before.length + insertion.length;
|
|
277
|
+
queueMicrotask(() => {
|
|
278
|
+
if (inputRef) inputRef.setSelectionRange(newPos, newPos);
|
|
279
|
+
setCursorPos(newPos);
|
|
280
|
+
});
|
|
281
|
+
inputRef?.focus();
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const activeItem = createMemo(() => resultItems()[activeIndex()] ?? null);
|
|
285
|
+
const listEntries = createMemo(() => buildListEntries(resultItems()));
|
|
286
|
+
|
|
287
|
+
// Empty-state branching. Single source of truth for what the body renders.
|
|
288
|
+
// - suggestions: user hasn't typed enough to search; show what's possible.
|
|
289
|
+
// - unsupported-tags: response told us no app accepts these tags.
|
|
290
|
+
// - no-results: search ran, returned nothing.
|
|
291
|
+
// - ready: render results.
|
|
292
|
+
// - idle: search debounced or in flight, no decision yet.
|
|
293
|
+
type BodyMode = "suggestions" | "unsupported-tags" | "no-results" | "ready" | "idle";
|
|
294
|
+
const bodyMode = createMemo<BodyMode>(() => {
|
|
295
|
+
if (!canSearch()) return "suggestions";
|
|
296
|
+
if (resultItems().length > 0) return "ready";
|
|
297
|
+
if (searchQuery.loading() || searchQuery.refreshing() || (!searchQuery.data() && !searchQuery.error())) return "idle";
|
|
298
|
+
if (unsupportedTags().length > 0) return "unsupported-tags";
|
|
299
|
+
return "no-results";
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const { debouncedFn: debounceSearch, cancel: cancelDebounce } = timed.debounce(
|
|
303
|
+
(request: { input: ParsedInput; appId: string | null }) => {
|
|
304
|
+
setSearchUrl(cloudResourceSearchUrl({ ...request.input, appId: request.appId, requireReader: props.requireReader }));
|
|
305
|
+
},
|
|
306
|
+
SEARCH_DEBOUNCE_MS,
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const bindRowRef = (key: string, element?: HTMLButtonElement) => {
|
|
310
|
+
if (!element) {
|
|
311
|
+
rowRefs.delete(key);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
rowRefs.set(key, element);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const selectItem = (row?: SearchItem) => {
|
|
318
|
+
if (!row) return;
|
|
319
|
+
props.onSelect(row);
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const moveSelection = (delta: -1 | 1) => {
|
|
323
|
+
const list = resultItems();
|
|
324
|
+
if (list.length === 0) return;
|
|
325
|
+
const next = (activeIndex() + delta + list.length) % list.length;
|
|
326
|
+
setActiveIndex(next);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const removeTag = (tag: string) => {
|
|
330
|
+
setRawInput((prev) => removeTagFromInput(prev, tag));
|
|
331
|
+
inputRef?.focus();
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
335
|
+
// Tag autocomplete intercepts navigation when the popover is open. Only
|
|
336
|
+
// reaches results-list nav when no suggestion is being chosen.
|
|
337
|
+
if (popoverOpen()) {
|
|
338
|
+
if (event.key === "ArrowDown") {
|
|
339
|
+
event.preventDefault();
|
|
340
|
+
const list = filteredSuggestions();
|
|
341
|
+
setSuggestionIndex((i) => (i + 1) % list.length);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (event.key === "ArrowUp") {
|
|
345
|
+
event.preventDefault();
|
|
346
|
+
const list = filteredSuggestions();
|
|
347
|
+
setSuggestionIndex((i) => (i - 1 + list.length) % list.length);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (event.key === "Enter" || event.key === "Tab") {
|
|
351
|
+
event.preventDefault();
|
|
352
|
+
const list = filteredSuggestions();
|
|
353
|
+
const choice = list[suggestionIndex()];
|
|
354
|
+
if (choice) insertTagAtCursor(choice.tag);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (event.key === "Escape") {
|
|
358
|
+
event.preventDefault();
|
|
359
|
+
// Close popover by erasing the in-flight tag token (caret-prefixed
|
|
360
|
+
// `#xyz`) — leaves the rest of the query intact. If the user wants
|
|
361
|
+
// to keep `#xyz`, they can press Space instead.
|
|
362
|
+
const ctx = tagContext();
|
|
363
|
+
if (ctx) {
|
|
364
|
+
const text = rawInput();
|
|
365
|
+
const next = text.slice(0, ctx.start) + text.slice(ctx.end).replace(/^\s+/, "");
|
|
366
|
+
setRawInput(next);
|
|
367
|
+
queueMicrotask(() => {
|
|
368
|
+
if (inputRef) inputRef.setSelectionRange(ctx.start, ctx.start);
|
|
369
|
+
setCursorPos(ctx.start);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
// Space commits the current token as-is and closes the popover by
|
|
375
|
+
// virtue of advancing the cursor past `#`. No special handling needed —
|
|
376
|
+
// tagContext recomputes naturally.
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (event.key === "ArrowDown") {
|
|
380
|
+
event.preventDefault();
|
|
381
|
+
moveSelection(1);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (event.key === "ArrowUp") {
|
|
386
|
+
event.preventDefault();
|
|
387
|
+
moveSelection(-1);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (event.key === "Enter") {
|
|
392
|
+
event.preventDefault();
|
|
393
|
+
selectItem(activeItem() ?? undefined);
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
createEffect(() => {
|
|
398
|
+
const response = searchQuery.data();
|
|
399
|
+
if (response) setActiveIndex(0);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
createEffect(() => {
|
|
403
|
+
const maxIndex = resultItems().length - 1;
|
|
404
|
+
if (maxIndex < 0) {
|
|
405
|
+
setActiveIndex(0);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (activeIndex() > maxIndex) setActiveIndex(maxIndex);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
createEffect(() => {
|
|
413
|
+
const item = activeItem();
|
|
414
|
+
const key = item ? rowKey(item) : null;
|
|
415
|
+
|
|
416
|
+
setPreviewFailed(false);
|
|
417
|
+
if (!key) return;
|
|
418
|
+
|
|
419
|
+
queueMicrotask(() => {
|
|
420
|
+
rowRefs.get(key)?.scrollIntoView({ block: "nearest" });
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
createEffect(() => {
|
|
425
|
+
const input = parsedInput();
|
|
426
|
+
const selectedAppId = appId();
|
|
427
|
+
|
|
428
|
+
if (!canSearch()) {
|
|
429
|
+
cancelDebounce();
|
|
430
|
+
setSearchUrl(cloudResourceSearchUrl({ ...input, requireReader: props.requireReader }));
|
|
431
|
+
setActiveIndex(0);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
debounceSearch({ input, appId: selectedAppId });
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
onCleanup(() => {
|
|
439
|
+
cancelDebounce();
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
onMount(() => {
|
|
443
|
+
requestAnimationFrame(() => inputRef?.focus());
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
return (
|
|
447
|
+
<div
|
|
448
|
+
class="grid h-full min-h-0 grid-cols-1 grid-rows-[auto_minmax(0,1fr)] text-zinc-900 dark:text-zinc-100"
|
|
449
|
+
classList={{
|
|
450
|
+
"md:grid-cols-[minmax(0,1fr)_18rem] md:gap-x-3": bodyMode() === "ready",
|
|
451
|
+
}}
|
|
452
|
+
onWheel={(event) => event.stopPropagation()}
|
|
453
|
+
>
|
|
454
|
+
<div class="flex min-w-0 items-center gap-2 pr-3">
|
|
455
|
+
<div class="relative min-w-0 flex-1">
|
|
456
|
+
<label class="flex items-center gap-3 px-4 py-3.5">
|
|
457
|
+
<i class="ti ti-search text-xl text-dimmed" />
|
|
458
|
+
<input
|
|
459
|
+
id="spotlight-input"
|
|
460
|
+
ref={inputRef}
|
|
461
|
+
type="search"
|
|
462
|
+
value={rawInput()}
|
|
463
|
+
onInput={(event) => {
|
|
464
|
+
setRawInput(event.currentTarget.value);
|
|
465
|
+
setCursorPos(event.currentTarget.selectionStart ?? event.currentTarget.value.length);
|
|
466
|
+
}}
|
|
467
|
+
onKeyUp={updateCursor}
|
|
468
|
+
onClick={updateCursor}
|
|
469
|
+
onSelect={updateCursor}
|
|
470
|
+
onKeyDown={handleKeyDown}
|
|
471
|
+
placeholder={props.placeholder ?? t().searchPlaceholder}
|
|
472
|
+
aria-label={t().searchCloudResources}
|
|
473
|
+
class="w-full border-0 bg-transparent text-base outline-none placeholder:text-dimmed md:text-lg"
|
|
474
|
+
spellcheck={false}
|
|
475
|
+
autocapitalize="off"
|
|
476
|
+
autocomplete="off"
|
|
477
|
+
autocorrect="off"
|
|
478
|
+
/>
|
|
479
|
+
<Show when={searchQuery.loading() || searchQuery.refreshing()}>
|
|
480
|
+
<i class="ti ti-loader-2 animate-spin text-dimmed" />
|
|
481
|
+
</Show>
|
|
482
|
+
</label>
|
|
483
|
+
|
|
484
|
+
{/* Tag autocomplete popover. Anchored to the input row, overlays the
|
|
485
|
+
body when the user is mid-typing a `#tag` token. Keyboard nav
|
|
486
|
+
(Up/Down/Tab/Enter/Escape) is intercepted by handleKeyDown. */}
|
|
487
|
+
<Show when={popoverOpen()}>
|
|
488
|
+
<div
|
|
489
|
+
class="absolute left-3 right-3 top-full z-30 -mt-1 max-h-64 overflow-y-auto overscroll-contain rounded-xl bg-white/95 p-1.5 shadow-lg ring-1 ring-inset ring-zinc-300/60 backdrop-blur-sm dark:bg-zinc-900/95 dark:ring-zinc-700/60"
|
|
490
|
+
role="listbox"
|
|
491
|
+
aria-label={t().tagSuggestions}
|
|
492
|
+
>
|
|
493
|
+
<For each={filteredSuggestions()}>
|
|
494
|
+
{(suggestion, index) => {
|
|
495
|
+
const selected = () => index() === suggestionIndex();
|
|
496
|
+
return (
|
|
497
|
+
<button
|
|
498
|
+
type="button"
|
|
499
|
+
role="option"
|
|
500
|
+
aria-selected={selected()}
|
|
501
|
+
onMouseEnter={() => setSuggestionIndex(index())}
|
|
502
|
+
onMouseDown={(e) => {
|
|
503
|
+
// mousedown so the input doesn't lose focus to the click.
|
|
504
|
+
e.preventDefault();
|
|
505
|
+
insertTagAtCursor(suggestion.tag);
|
|
506
|
+
}}
|
|
507
|
+
class="flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-left text-xs transition-colors"
|
|
508
|
+
classList={{
|
|
509
|
+
"bg-blue-50/85 dark:bg-blue-950/45": selected(),
|
|
510
|
+
"hover:bg-zinc-100/85 dark:hover:bg-zinc-800/60": !selected(),
|
|
511
|
+
}}
|
|
512
|
+
>
|
|
513
|
+
<i class={`${suggestion.appIcon} text-[12px] text-dimmed`} />
|
|
514
|
+
<span class="font-medium">#{suggestion.tag}</span>
|
|
515
|
+
<span class="text-[10px] text-dimmed">{suggestion.appName}</span>
|
|
516
|
+
</button>
|
|
517
|
+
);
|
|
518
|
+
}}
|
|
519
|
+
</For>
|
|
520
|
+
</div>
|
|
521
|
+
</Show>
|
|
522
|
+
</div>
|
|
523
|
+
<Select
|
|
524
|
+
class="w-44 shrink-0"
|
|
525
|
+
aria-label={t().filterByApp}
|
|
526
|
+
value={appId}
|
|
527
|
+
onValueChange={setAppId}
|
|
528
|
+
placeholder={t().allApps}
|
|
529
|
+
clearable
|
|
530
|
+
options={apps().map((app) => ({ id: app.id, label: app.name, icon: app.icon }))}
|
|
531
|
+
/>
|
|
532
|
+
</div>
|
|
533
|
+
|
|
534
|
+
<div class="min-h-0 overflow-hidden">
|
|
535
|
+
<div class="flex h-full min-h-0 flex-col gap-2 px-3 pb-3">
|
|
536
|
+
{/* Active-tag chip row + meta. Hidden in suggestions mode. */}
|
|
537
|
+
<Show when={bodyMode() !== "suggestions"}>
|
|
538
|
+
<div class="flex h-8 items-center justify-between gap-3">
|
|
539
|
+
<div class="flex min-w-0 flex-wrap items-center gap-2 text-[11px] text-dimmed">
|
|
540
|
+
<Show when={parsedInput().tags.length > 0}>
|
|
541
|
+
<div class="flex flex-wrap items-center gap-1">
|
|
542
|
+
<For each={parsedInput().tags}>
|
|
543
|
+
{(tag) => {
|
|
544
|
+
const isUnsupported = () => unsupportedTags().includes(tag);
|
|
545
|
+
return (
|
|
546
|
+
<span
|
|
547
|
+
class="group inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px]"
|
|
548
|
+
classList={{
|
|
549
|
+
"bg-zinc-200/70 dark:bg-zinc-800/70": !isUnsupported(),
|
|
550
|
+
"bg-amber-200/60 text-amber-900 dark:bg-amber-900/35 dark:text-amber-200": isUnsupported(),
|
|
551
|
+
}}
|
|
552
|
+
>
|
|
553
|
+
#{tag}
|
|
554
|
+
<button
|
|
555
|
+
type="button"
|
|
556
|
+
class="opacity-60 hover:opacity-100"
|
|
557
|
+
onClick={() => removeTag(tag)}
|
|
558
|
+
aria-label={t().removeTag({ tag })}
|
|
559
|
+
title={t().removeTagTitle({ tag })}
|
|
560
|
+
>
|
|
561
|
+
<i class="ti ti-x text-[10px]" />
|
|
562
|
+
</button>
|
|
563
|
+
</span>
|
|
564
|
+
);
|
|
565
|
+
}}
|
|
566
|
+
</For>
|
|
567
|
+
</div>
|
|
568
|
+
</Show>
|
|
569
|
+
<Show when={bodyMode() === "ready"}>
|
|
570
|
+
<span>
|
|
571
|
+
{t().resultCount({ count: resultItems().length })} <span aria-hidden="true">•</span>{" "}
|
|
572
|
+
<Show when={props.onHelp}>
|
|
573
|
+
<button type="button" class="text-blue-500 hover:underline dark:text-blue-400" onClick={() => props.onHelp?.()}>
|
|
574
|
+
{t().tagHelp}
|
|
575
|
+
</button>
|
|
576
|
+
</Show>
|
|
577
|
+
</span>
|
|
578
|
+
</Show>
|
|
579
|
+
</div>
|
|
580
|
+
</div>
|
|
581
|
+
</Show>
|
|
582
|
+
|
|
583
|
+
<div class="min-h-0 flex-1 overflow-hidden">
|
|
584
|
+
<Show when={searchQuery.error()}>
|
|
585
|
+
{(error) => (
|
|
586
|
+
<NoticeCard tone="danger" icon={false} class="mb-2">
|
|
587
|
+
{error().message}
|
|
588
|
+
</NoticeCard>
|
|
589
|
+
)}
|
|
590
|
+
</Show>
|
|
591
|
+
|
|
592
|
+
{/* Suggestions: empty input, show available tags as clickable chips. */}
|
|
593
|
+
<Show when={bodyMode() === "suggestions"}>
|
|
594
|
+
<ScrollArea class="flex h-full min-h-0 flex-col gap-3 pr-1">
|
|
595
|
+
<p class="text-xs text-dimmed">
|
|
596
|
+
{t().typeToSearchBeforeTag} <code class="rounded bg-zinc-100 px-1 py-0.5 text-[10px] dark:bg-zinc-900">#tag</code>{" "}
|
|
597
|
+
{t().typeToSearchAfterTag}
|
|
598
|
+
</p>
|
|
599
|
+
<Show when={tagSuggestions().length > 0} fallback={<p class="text-xs text-dimmed">{t().noTagsAvailable}</p>}>
|
|
600
|
+
<div class="flex flex-wrap gap-1">
|
|
601
|
+
<For each={tagSuggestions()}>
|
|
602
|
+
{(suggestion) => (
|
|
603
|
+
<button
|
|
604
|
+
type="button"
|
|
605
|
+
onClick={() => insertTagAtCursor(suggestion.tag)}
|
|
606
|
+
class="inline-flex items-center gap-1 rounded-full bg-zinc-100/80 px-2 py-0.5 text-[10px] leading-4 text-zinc-700 transition-colors hover:bg-zinc-200/80 dark:bg-zinc-900/55 dark:text-zinc-200 dark:hover:bg-zinc-800/80"
|
|
607
|
+
title={t().addTag({ tag: suggestion.tag, app: suggestion.appName })}
|
|
608
|
+
>
|
|
609
|
+
<i class={`${suggestion.appIcon} text-[10px] text-dimmed`} />
|
|
610
|
+
<span>#{suggestion.tag}</span>
|
|
611
|
+
</button>
|
|
612
|
+
)}
|
|
613
|
+
</For>
|
|
614
|
+
</div>
|
|
615
|
+
</Show>
|
|
616
|
+
<Show when={props.onHelp}>
|
|
617
|
+
<button
|
|
618
|
+
type="button"
|
|
619
|
+
class="self-start text-[11px] text-blue-500 hover:underline dark:text-blue-400"
|
|
620
|
+
onClick={() => props.onHelp?.()}
|
|
621
|
+
>
|
|
622
|
+
{t().seeAllTagDescriptions}
|
|
623
|
+
</button>
|
|
624
|
+
</Show>
|
|
625
|
+
</ScrollArea>
|
|
626
|
+
</Show>
|
|
627
|
+
|
|
628
|
+
{/* Unsupported tags: response told us no app handles them. */}
|
|
629
|
+
<Show when={bodyMode() === "unsupported-tags"}>
|
|
630
|
+
<div class="flex h-full min-h-0 flex-col items-center justify-center gap-3 text-center">
|
|
631
|
+
<i class="ti ti-tag-off text-2xl text-dimmed" />
|
|
632
|
+
<div class="flex flex-col gap-1">
|
|
633
|
+
<p class="text-xs">
|
|
634
|
+
{t().noAppSupports}{" "}
|
|
635
|
+
<For each={unsupportedTags()}>
|
|
636
|
+
{(tag, index) => (
|
|
637
|
+
<>
|
|
638
|
+
<code class="rounded bg-amber-200/60 px-1 py-0.5 text-[10px] text-amber-900 dark:bg-amber-900/35 dark:text-amber-200">
|
|
639
|
+
#{tag}
|
|
640
|
+
</code>
|
|
641
|
+
<Show when={index() < unsupportedTags().length - 1}> </Show>
|
|
642
|
+
</>
|
|
643
|
+
)}
|
|
644
|
+
</For>
|
|
645
|
+
.
|
|
646
|
+
</p>
|
|
647
|
+
<p class="text-[11px] text-dimmed">{t().removeTagOrPick}</p>
|
|
648
|
+
</div>
|
|
649
|
+
<div class="flex flex-wrap items-center justify-center gap-1.5">
|
|
650
|
+
<For each={unsupportedTags()}>
|
|
651
|
+
{(tag) => (
|
|
652
|
+
<button
|
|
653
|
+
type="button"
|
|
654
|
+
onClick={() => removeTag(tag)}
|
|
655
|
+
class="inline-flex items-center gap-1 rounded-full bg-amber-200/60 px-2.5 py-1 text-[11px] text-amber-900 hover:bg-amber-300/60 dark:bg-amber-900/35 dark:text-amber-200 dark:hover:bg-amber-900/55"
|
|
656
|
+
>
|
|
657
|
+
<span>{t().removeNamedTag({ tag })}</span>
|
|
658
|
+
<i class="ti ti-x text-[10px]" />
|
|
659
|
+
</button>
|
|
660
|
+
)}
|
|
661
|
+
</For>
|
|
662
|
+
</div>
|
|
663
|
+
</div>
|
|
664
|
+
</Show>
|
|
665
|
+
|
|
666
|
+
{/* No matches at all (query/tags valid, just empty result set). */}
|
|
667
|
+
<Show when={bodyMode() === "no-results"}>
|
|
668
|
+
<div class="flex h-full min-h-0 flex-col items-center justify-center gap-2 text-center">
|
|
669
|
+
<i class="ti ti-mood-empty text-2xl text-dimmed" />
|
|
670
|
+
<p class="text-xs text-dimmed">
|
|
671
|
+
{t().noMatches}
|
|
672
|
+
{parsedInput().query.length > 0 ? (
|
|
673
|
+
<>
|
|
674
|
+
{" "}
|
|
675
|
+
{t().matchesFor} <span class="text-zinc-700 dark:text-zinc-200">"{parsedInput().query}"</span>
|
|
676
|
+
</>
|
|
677
|
+
) : null}
|
|
678
|
+
.
|
|
679
|
+
</p>
|
|
680
|
+
</div>
|
|
681
|
+
</Show>
|
|
682
|
+
|
|
683
|
+
{/* Ready: the left result list; the detail spans both grid rows below. */}
|
|
684
|
+
<Show when={bodyMode() === "ready"}>
|
|
685
|
+
<div class="h-full min-h-0">
|
|
686
|
+
<section class="h-full min-h-0 overflow-y-auto overscroll-y-contain pr-1" onWheel={(event) => event.stopPropagation()}>
|
|
687
|
+
<div class="flex flex-col">
|
|
688
|
+
<For each={listEntries()}>
|
|
689
|
+
{(entry) => {
|
|
690
|
+
if (entry.kind === "header") {
|
|
691
|
+
// Faint section header — no <hr>, no border. First
|
|
692
|
+
// header gets less top spacing so it doesn't shove
|
|
693
|
+
// the list down on group transitions.
|
|
694
|
+
return (
|
|
695
|
+
<div class="flex items-center gap-1.5 px-1 pt-3 pb-1 text-[10px] uppercase tracking-wide text-dimmed first:pt-1">
|
|
696
|
+
<i class={`${entry.appIcon} text-[11px]`} />
|
|
697
|
+
<span>{entry.appName}</span>
|
|
698
|
+
<span class="opacity-60">· {entry.count}</span>
|
|
699
|
+
</div>
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
const item = entry.item;
|
|
703
|
+
const selected = () => entry.flatIndex === activeIndex();
|
|
704
|
+
return (
|
|
705
|
+
<button
|
|
706
|
+
ref={(element) => bindRowRef(rowKey(item), element)}
|
|
707
|
+
type="button"
|
|
708
|
+
onMouseEnter={() => setActiveIndex(entry.flatIndex)}
|
|
709
|
+
onClick={() => selectItem(item)}
|
|
710
|
+
class="mt-1.5 w-full rounded-xl p-2.5 text-left transition-colors first:mt-0"
|
|
711
|
+
classList={{
|
|
712
|
+
"bg-blue-50/85 dark:bg-blue-950/45": selected(),
|
|
713
|
+
"bg-zinc-50/75 hover:bg-zinc-100/85 dark:bg-zinc-900/45 dark:hover:bg-zinc-900/65": !selected(),
|
|
714
|
+
}}
|
|
715
|
+
>
|
|
716
|
+
<div class="flex items-start gap-2.5">
|
|
717
|
+
<i class={`${item.icon ?? item.appIcon} mt-0.5 text-[13px] text-dimmed`} />
|
|
718
|
+
<div class="min-w-0">
|
|
719
|
+
<p class="truncate text-xs">{item.title}</p>
|
|
720
|
+
<Show when={item.preview}>
|
|
721
|
+
<p class="mt-0.5 truncate text-[11px] text-dimmed">{item.preview}</p>
|
|
722
|
+
</Show>
|
|
723
|
+
<p class="mt-1 text-[10px] text-dimmed">{item.appName}</p>
|
|
724
|
+
</div>
|
|
725
|
+
</div>
|
|
726
|
+
</button>
|
|
727
|
+
);
|
|
728
|
+
}}
|
|
729
|
+
</For>
|
|
730
|
+
</div>
|
|
731
|
+
</section>
|
|
732
|
+
</div>
|
|
733
|
+
</Show>
|
|
734
|
+
</div>
|
|
735
|
+
</div>
|
|
736
|
+
</div>
|
|
737
|
+
|
|
738
|
+
<Show when={bodyMode() === "ready"}>
|
|
739
|
+
<aside
|
|
740
|
+
class="hidden min-h-0 overflow-y-auto overscroll-y-contain rounded-xl bg-zinc-50/80 p-3 dark:bg-zinc-900/55 md:col-start-2 md:row-start-1 md:row-span-2 md:my-3 md:mr-3 md:block"
|
|
741
|
+
onWheel={(event) => event.stopPropagation()}
|
|
742
|
+
>
|
|
743
|
+
<Show when={activeItem()} fallback={<div class="text-xs text-dimmed">{t().selectPreview}</div>}>
|
|
744
|
+
{(item) => (
|
|
745
|
+
<div class="flex flex-col gap-4">
|
|
746
|
+
<div class="flex items-center gap-3">
|
|
747
|
+
<div class="grid h-11 w-11 shrink-0 place-items-center overflow-hidden rounded-lg bg-zinc-100 dark:bg-zinc-900">
|
|
748
|
+
<Show
|
|
749
|
+
when={isValidImagePreviewUrl(item().previewUrl) && !previewFailed()}
|
|
750
|
+
fallback={<i class={`${item().icon ?? item().appIcon} text-lg text-dimmed`} />}
|
|
751
|
+
>
|
|
752
|
+
<img
|
|
753
|
+
src={item().previewUrl}
|
|
754
|
+
alt={item().title}
|
|
755
|
+
class="h-full w-full object-cover"
|
|
756
|
+
onError={() => setPreviewFailed(true)}
|
|
757
|
+
/>
|
|
758
|
+
</Show>
|
|
759
|
+
</div>
|
|
760
|
+
<div class="min-w-0">
|
|
761
|
+
<p class="truncate text-sm">{item().title}</p>
|
|
762
|
+
<p class="mt-0.5 truncate text-xs text-dimmed">{item().appName}</p>
|
|
763
|
+
</div>
|
|
764
|
+
</div>
|
|
765
|
+
|
|
766
|
+
<Show when={item().preview}>
|
|
767
|
+
<p class="text-xs leading-relaxed text-dimmed">{item().preview}</p>
|
|
768
|
+
</Show>
|
|
769
|
+
|
|
770
|
+
<Show when={metadataRows(item().metadata).length > 0}>
|
|
771
|
+
<div class="rounded-lg bg-zinc-100/65 p-2 dark:bg-zinc-900/65">
|
|
772
|
+
<div class="flex flex-col gap-1">
|
|
773
|
+
<For each={metadataRows(item().metadata)}>
|
|
774
|
+
{(entry) => (
|
|
775
|
+
<div class="grid grid-cols-[7rem_minmax(0,1fr)] items-center gap-2 rounded-md px-1 py-1 text-xs">
|
|
776
|
+
<span class="truncate text-dimmed">{entry.label}</span>
|
|
777
|
+
<span class="truncate">{entry.value}</span>
|
|
778
|
+
</div>
|
|
779
|
+
)}
|
|
780
|
+
</For>
|
|
781
|
+
</div>
|
|
782
|
+
</div>
|
|
783
|
+
</Show>
|
|
784
|
+
</div>
|
|
785
|
+
)}
|
|
786
|
+
</Show>
|
|
787
|
+
</aside>
|
|
788
|
+
</Show>
|
|
789
|
+
</div>
|
|
790
|
+
);
|
|
791
|
+
}
|