@agimon-ai/doompi-web 0.0.1-alpha.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/LICENSE +29 -0
- package/README.md +196 -0
- package/dist/_virtual/_rolldown/runtime.cjs +1 -0
- package/dist/bin/serve.cjs +3 -0
- package/dist/bin/serve.cjs.map +1 -0
- package/dist/bin/serve.d.cts +1 -0
- package/dist/bin/serve.d.mts +1 -0
- package/dist/bin/serve.mjs +3 -0
- package/dist/bin/serve.mjs.map +1 -0
- package/dist/bundler.cjs +1 -0
- package/dist/bundler.d.cts +2 -0
- package/dist/bundler.d.mts +2 -0
- package/dist/bundler.mjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +1 -0
- package/dist/src/adapters/authRoutes.cjs +2 -0
- package/dist/src/adapters/authRoutes.cjs.map +1 -0
- package/dist/src/adapters/authRoutes.mjs +2 -0
- package/dist/src/adapters/authRoutes.mjs.map +1 -0
- package/dist/src/adapters/bundledServer.cjs +2 -0
- package/dist/src/adapters/bundledServer.cjs.map +1 -0
- package/dist/src/adapters/bundledServer.mjs +2 -0
- package/dist/src/adapters/bundledServer.mjs.map +1 -0
- package/dist/src/adapters/directoryListing.cjs +2 -0
- package/dist/src/adapters/directoryListing.cjs.map +1 -0
- package/dist/src/adapters/directoryListing.mjs +2 -0
- package/dist/src/adapters/directoryListing.mjs.map +1 -0
- package/dist/src/adapters/directorySearch.cjs +3 -0
- package/dist/src/adapters/directorySearch.cjs.map +1 -0
- package/dist/src/adapters/directorySearch.mjs +3 -0
- package/dist/src/adapters/directorySearch.mjs.map +1 -0
- package/dist/src/adapters/gitStatus.cjs +2 -0
- package/dist/src/adapters/gitStatus.cjs.map +1 -0
- package/dist/src/adapters/gitStatus.mjs +2 -0
- package/dist/src/adapters/gitStatus.mjs.map +1 -0
- package/dist/src/adapters/httpServer.cjs +2 -0
- package/dist/src/adapters/httpServer.cjs.map +1 -0
- package/dist/src/adapters/httpServer.d.cts +14 -0
- package/dist/src/adapters/httpServer.d.cts.map +1 -0
- package/dist/src/adapters/httpServer.d.mts +14 -0
- package/dist/src/adapters/httpServer.d.mts.map +1 -0
- package/dist/src/adapters/httpServer.mjs +2 -0
- package/dist/src/adapters/httpServer.mjs.map +1 -0
- package/dist/src/adapters/hubProbe.cjs +2 -0
- package/dist/src/adapters/hubProbe.cjs.map +1 -0
- package/dist/src/adapters/hubProbe.mjs +2 -0
- package/dist/src/adapters/hubProbe.mjs.map +1 -0
- package/dist/src/adapters/packageApiProxy.cjs +2 -0
- package/dist/src/adapters/packageApiProxy.cjs.map +1 -0
- package/dist/src/adapters/packageApiProxy.mjs +2 -0
- package/dist/src/adapters/packageApiProxy.mjs.map +1 -0
- package/dist/src/adapters/piHubService.cjs +2 -0
- package/dist/src/adapters/piHubService.cjs.map +1 -0
- package/dist/src/adapters/piHubService.mjs +2 -0
- package/dist/src/adapters/piHubService.mjs.map +1 -0
- package/dist/src/adapters/piWebSocketListener.cjs +2 -0
- package/dist/src/adapters/piWebSocketListener.cjs.map +1 -0
- package/dist/src/adapters/piWebSocketListener.mjs +2 -0
- package/dist/src/adapters/piWebSocketListener.mjs.map +1 -0
- package/dist/src/adapters/providerAuth.cjs +2 -0
- package/dist/src/adapters/providerAuth.cjs.map +1 -0
- package/dist/src/adapters/providerAuth.mjs +2 -0
- package/dist/src/adapters/providerAuth.mjs.map +1 -0
- package/dist/src/adapters/registryWatcher.cjs +2 -0
- package/dist/src/adapters/registryWatcher.cjs.map +1 -0
- package/dist/src/adapters/registryWatcher.mjs +2 -0
- package/dist/src/adapters/registryWatcher.mjs.map +1 -0
- package/dist/src/adapters/serverSpawner.cjs +2 -0
- package/dist/src/adapters/serverSpawner.cjs.map +1 -0
- package/dist/src/adapters/serverSpawner.mjs +2 -0
- package/dist/src/adapters/serverSpawner.mjs.map +1 -0
- package/dist/src/adapters/sessionFiles.cjs +3 -0
- package/dist/src/adapters/sessionFiles.cjs.map +1 -0
- package/dist/src/adapters/sessionFiles.mjs +3 -0
- package/dist/src/adapters/sessionFiles.mjs.map +1 -0
- package/dist/src/adapters/sessionHub.cjs +2 -0
- package/dist/src/adapters/sessionHub.cjs.map +1 -0
- package/dist/src/adapters/sessionHub.mjs +2 -0
- package/dist/src/adapters/sessionHub.mjs.map +1 -0
- package/dist/src/adapters/sessionSocketClient.cjs +2 -0
- package/dist/src/adapters/sessionSocketClient.cjs.map +1 -0
- package/dist/src/adapters/sessionSocketClient.mjs +2 -0
- package/dist/src/adapters/sessionSocketClient.mjs.map +1 -0
- package/dist/src/adapters/syncGuard.cjs +2 -0
- package/dist/src/adapters/syncGuard.cjs.map +1 -0
- package/dist/src/adapters/syncGuard.mjs +2 -0
- package/dist/src/adapters/syncGuard.mjs.map +1 -0
- package/dist/src/adapters/threadJournals.cjs +2 -0
- package/dist/src/adapters/threadJournals.cjs.map +1 -0
- package/dist/src/adapters/threadJournals.mjs +2 -0
- package/dist/src/adapters/threadJournals.mjs.map +1 -0
- package/dist/src/adapters/webBundler.cjs +2 -0
- package/dist/src/adapters/webBundler.cjs.map +1 -0
- package/dist/src/adapters/webBundler.d.cts +28 -0
- package/dist/src/adapters/webBundler.d.cts.map +1 -0
- package/dist/src/adapters/webBundler.d.mts +28 -0
- package/dist/src/adapters/webBundler.d.mts.map +1 -0
- package/dist/src/adapters/webBundler.mjs +2 -0
- package/dist/src/adapters/webBundler.mjs.map +1 -0
- package/dist/src/adapters/webHubPluginLoader.cjs +2 -0
- package/dist/src/adapters/webHubPluginLoader.cjs.map +1 -0
- package/dist/src/adapters/webHubPluginLoader.mjs +2 -0
- package/dist/src/adapters/webHubPluginLoader.mjs.map +1 -0
- package/dist/src/adapters/webHubPlugins.generated.cjs +2 -0
- package/dist/src/adapters/webHubPlugins.generated.cjs.map +1 -0
- package/dist/src/adapters/webHubPlugins.generated.mjs +2 -0
- package/dist/src/adapters/webHubPlugins.generated.mjs.map +1 -0
- package/dist/src/adapters/webPluginGenerate.cjs +4 -0
- package/dist/src/adapters/webPluginGenerate.cjs.map +1 -0
- package/dist/src/adapters/webPluginGenerate.mjs +4 -0
- package/dist/src/adapters/webPluginGenerate.mjs.map +1 -0
- package/dist/src/adapters/webPluginScan.cjs +2 -0
- package/dist/src/adapters/webPluginScan.cjs.map +1 -0
- package/dist/src/adapters/webPluginScan.mjs +2 -0
- package/dist/src/adapters/webPluginScan.mjs.map +1 -0
- package/dist/src/adapters/webPluginVite.cjs +2 -0
- package/dist/src/adapters/webPluginVite.cjs.map +1 -0
- package/dist/src/adapters/webPluginVite.mjs +2 -0
- package/dist/src/adapters/webPluginVite.mjs.map +1 -0
- package/dist/src/services/directoryMatch.cjs +2 -0
- package/dist/src/services/directoryMatch.cjs.map +1 -0
- package/dist/src/services/directoryMatch.mjs +2 -0
- package/dist/src/services/directoryMatch.mjs.map +1 -0
- package/dist/src/services/fileMatch.cjs +2 -0
- package/dist/src/services/fileMatch.cjs.map +1 -0
- package/dist/src/services/fileMatch.mjs +2 -0
- package/dist/src/services/fileMatch.mjs.map +1 -0
- package/dist/src/services/fileMedia.cjs +2 -0
- package/dist/src/services/fileMedia.cjs.map +1 -0
- package/dist/src/services/fileMedia.mjs +2 -0
- package/dist/src/services/fileMedia.mjs.map +1 -0
- package/dist/src/services/frameRing.cjs +2 -0
- package/dist/src/services/frameRing.cjs.map +1 -0
- package/dist/src/services/frameRing.mjs +2 -0
- package/dist/src/services/frameRing.mjs.map +1 -0
- package/dist/src/services/journalTail.cjs +3 -0
- package/dist/src/services/journalTail.cjs.map +1 -0
- package/dist/src/services/journalTail.mjs +3 -0
- package/dist/src/services/journalTail.mjs.map +1 -0
- package/dist/src/services/loginFlow.cjs +2 -0
- package/dist/src/services/loginFlow.cjs.map +1 -0
- package/dist/src/services/loginFlow.mjs +2 -0
- package/dist/src/services/loginFlow.mjs.map +1 -0
- package/dist/src/services/registryStore.cjs +2 -0
- package/dist/src/services/registryStore.cjs.map +1 -0
- package/dist/src/services/registryStore.mjs +2 -0
- package/dist/src/services/registryStore.mjs.map +1 -0
- package/dist/src/services/retryPolicy.cjs +2 -0
- package/dist/src/services/retryPolicy.cjs.map +1 -0
- package/dist/src/services/retryPolicy.mjs +2 -0
- package/dist/src/services/retryPolicy.mjs.map +1 -0
- package/dist/src/services/serveOptions.cjs +2 -0
- package/dist/src/services/serveOptions.cjs.map +1 -0
- package/dist/src/services/serveOptions.d.cts +21 -0
- package/dist/src/services/serveOptions.d.cts.map +1 -0
- package/dist/src/services/serveOptions.d.mts +21 -0
- package/dist/src/services/serveOptions.d.mts.map +1 -0
- package/dist/src/services/serveOptions.mjs +2 -0
- package/dist/src/services/serveOptions.mjs.map +1 -0
- package/dist/src/services/sessionFraming.cjs +4 -0
- package/dist/src/services/sessionFraming.cjs.map +1 -0
- package/dist/src/services/sessionFraming.mjs +4 -0
- package/dist/src/services/sessionFraming.mjs.map +1 -0
- package/dist/src/services/sessionPresence.cjs +2 -0
- package/dist/src/services/sessionPresence.cjs.map +1 -0
- package/dist/src/services/sessionPresence.mjs +2 -0
- package/dist/src/services/sessionPresence.mjs.map +1 -0
- package/dist/src/services/staticAssets.cjs +2 -0
- package/dist/src/services/staticAssets.cjs.map +1 -0
- package/dist/src/services/staticAssets.mjs +2 -0
- package/dist/src/services/staticAssets.mjs.map +1 -0
- package/dist/src/services/webDevRoots.cjs +2 -0
- package/dist/src/services/webDevRoots.cjs.map +1 -0
- package/dist/src/services/webDevRoots.mjs +2 -0
- package/dist/src/services/webDevRoots.mjs.map +1 -0
- package/dist/src/services/webPluginManifest.cjs +2 -0
- package/dist/src/services/webPluginManifest.cjs.map +1 -0
- package/dist/src/services/webPluginManifest.mjs +2 -0
- package/dist/src/services/webPluginManifest.mjs.map +1 -0
- package/dist/src/types/auth.cjs +2 -0
- package/dist/src/types/auth.cjs.map +1 -0
- package/dist/src/types/auth.d.cts +94 -0
- package/dist/src/types/auth.d.cts.map +1 -0
- package/dist/src/types/auth.d.mts +94 -0
- package/dist/src/types/auth.d.mts.map +1 -0
- package/dist/src/types/auth.mjs +2 -0
- package/dist/src/types/auth.mjs.map +1 -0
- package/dist/src/types/bridge.d.cts +31 -0
- package/dist/src/types/bridge.d.cts.map +1 -0
- package/dist/src/types/bridge.d.mts +31 -0
- package/dist/src/types/bridge.d.mts.map +1 -0
- package/dist/src/types/hub.cjs +2 -0
- package/dist/src/types/hub.cjs.map +1 -0
- package/dist/src/types/hub.d.cts +58 -0
- package/dist/src/types/hub.d.cts.map +1 -0
- package/dist/src/types/hub.d.mts +58 -0
- package/dist/src/types/hub.d.mts.map +1 -0
- package/dist/src/types/hub.mjs +2 -0
- package/dist/src/types/hub.mjs.map +1 -0
- package/dist/src/types/media.cjs +2 -0
- package/dist/src/types/media.cjs.map +1 -0
- package/dist/src/types/media.mjs +2 -0
- package/dist/src/types/media.mjs.map +1 -0
- package/dist/src/types/registry.cjs +1 -0
- package/dist/src/types/registry.d.cts +2 -0
- package/dist/src/types/registry.d.mts +2 -0
- package/dist/src/types/registry.mjs +1 -0
- package/dist/src/types/session.cjs +2 -0
- package/dist/src/types/session.cjs.map +1 -0
- package/dist/src/types/session.d.cts +24 -0
- package/dist/src/types/session.d.cts.map +1 -0
- package/dist/src/types/session.d.mts +24 -0
- package/dist/src/types/session.d.mts.map +1 -0
- package/dist/src/types/session.mjs +2 -0
- package/dist/src/types/session.mjs.map +1 -0
- package/dist/web/apple-touch-icon.png +0 -0
- package/dist/web/assets/index-Cg52ngKv.css +2 -0
- package/dist/web/assets/index-DQO7NTIP.js +90 -0
- package/dist/web/assets/index-DQO7NTIP.js.map +1 -0
- package/dist/web/favicon-16.png +0 -0
- package/dist/web/favicon-32.png +0 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/favicon.svg +16 -0
- package/dist/web/icon-512.png +0 -0
- package/dist/web/index.html +17 -0
- package/llms.txt +8 -0
- package/package.json +106 -0
- package/src/adapters/authRoutes.ts +95 -0
- package/src/adapters/bundledServer.ts +64 -0
- package/src/adapters/directoryListing.ts +127 -0
- package/src/adapters/directorySearch.ts +124 -0
- package/src/adapters/gitStatus.ts +27 -0
- package/src/adapters/httpServer.ts +556 -0
- package/src/adapters/hubProbe.ts +27 -0
- package/src/adapters/packageApiProxy.ts +60 -0
- package/src/adapters/piHubService.ts +129 -0
- package/src/adapters/piWebSocketListener.ts +98 -0
- package/src/adapters/providerAuth.ts +181 -0
- package/src/adapters/registryWatcher.ts +112 -0
- package/src/adapters/serverSpawner.ts +181 -0
- package/src/adapters/sessionFiles.ts +99 -0
- package/src/adapters/sessionHub.ts +567 -0
- package/src/adapters/sessionSocketClient.ts +105 -0
- package/src/adapters/syncGuard.ts +119 -0
- package/src/adapters/threadJournals.ts +184 -0
- package/src/adapters/webBundler.ts +83 -0
- package/src/adapters/webHubPluginLoader.ts +67 -0
- package/src/adapters/webHubPlugins.generated.ts +4 -0
- package/src/adapters/webPluginGenerate.ts +172 -0
- package/src/adapters/webPluginScan.ts +62 -0
- package/src/adapters/webPluginVite.ts +51 -0
- package/src/bin/serve.ts +64 -0
- package/src/exports/bundler.ts +1 -0
- package/src/exports/index.ts +9 -0
- package/src/services/directoryMatch.ts +71 -0
- package/src/services/fileMatch.ts +37 -0
- package/src/services/fileMedia.ts +27 -0
- package/src/services/frameRing.ts +33 -0
- package/src/services/journalTail.ts +47 -0
- package/src/services/loginFlow.ts +144 -0
- package/src/services/registryStore.ts +83 -0
- package/src/services/retryPolicy.ts +15 -0
- package/src/services/serveOptions.ts +63 -0
- package/src/services/sessionFraming.ts +32 -0
- package/src/services/sessionPresence.ts +131 -0
- package/src/services/staticAssets.ts +37 -0
- package/src/services/webDevRoots.ts +36 -0
- package/src/services/webPluginManifest.ts +149 -0
- package/src/types/auth.ts +118 -0
- package/src/types/bridge.ts +41 -0
- package/src/types/hub.ts +290 -0
- package/src/types/media.ts +38 -0
- package/src/types/registry.ts +2 -0
- package/src/types/session.ts +36 -0
- package/src/web/app/Providers.tsx +46 -0
- package/src/web/app/protocolRuntime.ts +106 -0
- package/src/web/app/sessionRuntime.ts +189 -0
- package/src/web/app/webPlugins.generated.ts +4 -0
- package/src/web/components/MascotMark.tsx +46 -0
- package/src/web/components/PluginSurface.tsx +10 -0
- package/src/web/components/ToolRendererBoundary.tsx +32 -0
- package/src/web/features/activity/ActivityDock.tsx +125 -0
- package/src/web/features/connection/RefusedCard.tsx +63 -0
- package/src/web/features/dialogs/DialogOverlay.tsx +176 -0
- package/src/web/features/leader/CommandPalette.tsx +386 -0
- package/src/web/features/selection/SelectionBar.tsx +529 -0
- package/src/web/features/session/Composer.tsx +368 -0
- package/src/web/features/session/MentionPreviews.tsx +58 -0
- package/src/web/features/session/ThreadView.tsx +35 -0
- package/src/web/features/session/Timeline.tsx +382 -0
- package/src/web/features/session/ToolCard.tsx +79 -0
- package/src/web/features/sessions/NewSessionDialog.tsx +222 -0
- package/src/web/features/sessions/SessionRail.tsx +411 -0
- package/src/web/features/settings/AppearanceSettings.tsx +88 -0
- package/src/web/features/settings/LoginFlowDialog.tsx +221 -0
- package/src/web/features/settings/PluginSettings.tsx +92 -0
- package/src/web/features/settings/ProviderSettings.tsx +238 -0
- package/src/web/features/settings/SettingsMenu.tsx +32 -0
- package/src/web/features/status/TopBar.tsx +208 -0
- package/src/web/index.html +16 -0
- package/src/web/lib/authApi.ts +90 -0
- package/src/web/lib/commands.ts +61 -0
- package/src/web/lib/composition.ts +225 -0
- package/src/web/lib/fileMentions.ts +32 -0
- package/src/web/lib/hubApi.ts +86 -0
- package/src/web/lib/leaderTree.ts +159 -0
- package/src/web/lib/piTransport.ts +61 -0
- package/src/web/lib/pluginRegistry.ts +488 -0
- package/src/web/lib/pluginSlotProps.ts +50 -0
- package/src/web/lib/promptFocus.ts +25 -0
- package/src/web/lib/protocolTimeline.ts +73 -0
- package/src/web/lib/sessionModel.ts +673 -0
- package/src/web/lib/sessionSummary.ts +58 -0
- package/src/web/lib/settingsSections.ts +21 -0
- package/src/web/lib/statusLine.ts +90 -0
- package/src/web/lib/threadRenderer.ts +25 -0
- package/src/web/lib/toolMessageProps.ts +21 -0
- package/src/web/lib/transport.ts +30 -0
- package/src/web/lib/wsClient.ts +73 -0
- package/src/web/main.tsx +17 -0
- package/src/web/public/apple-touch-icon.png +0 -0
- package/src/web/public/favicon-16.png +0 -0
- package/src/web/public/favicon-32.png +0 -0
- package/src/web/public/favicon.ico +0 -0
- package/src/web/public/favicon.svg +16 -0
- package/src/web/public/icon-512.png +0 -0
- package/src/web/routes/CockpitPage.tsx +98 -0
- package/src/web/routes/SettingsPage.tsx +60 -0
- package/src/web/routes/routeTree.tsx +49 -0
- package/src/web/stores/menuStore.ts +78 -0
- package/src/web/stores/paletteStore.ts +25 -0
- package/src/web/stores/sessionStore.ts +303 -0
- package/src/web/stores/sessionsStore.ts +155 -0
- package/src/web/stores/themeStore.ts +52 -0
- package/src/web/stores/threadStore.ts +68 -0
- package/src/web/stores/transientTabsStore.ts +63 -0
- package/src/web/stores/uiStore.ts +28 -0
- package/src/web/stores/useOpenTab.ts +19 -0
- package/src/web/stores/usePluginSlotProps.ts +22 -0
- package/src/web/styles/app.css +15 -0
- package/src/web/styles/webPluginSources.generated.css +1 -0
- package/src/web/vite-env.d.ts +1 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BranchIcon,
|
|
3
|
+
Button,
|
|
4
|
+
buttonVariants,
|
|
5
|
+
cn,
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogBody,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogDescription,
|
|
10
|
+
DialogFooter,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
DropdownMenu,
|
|
14
|
+
DropdownMenuContent,
|
|
15
|
+
DropdownMenuItem,
|
|
16
|
+
DropdownMenuTrigger,
|
|
17
|
+
GearIcon,
|
|
18
|
+
Input,
|
|
19
|
+
KebabIcon,
|
|
20
|
+
PlusIcon,
|
|
21
|
+
SectionLabel,
|
|
22
|
+
Tooltip,
|
|
23
|
+
TooltipContent,
|
|
24
|
+
TooltipTrigger,
|
|
25
|
+
} from '@agimon-ai/doompi-web-components';
|
|
26
|
+
import { Link, useNavigate } from '@tanstack/react-router';
|
|
27
|
+
import { useStore } from '@tanstack/react-store';
|
|
28
|
+
import { useEffect, useRef, useState } from 'react';
|
|
29
|
+
import { MascotMark } from '../../components/MascotMark.tsx';
|
|
30
|
+
import { PluginSurface } from '../../components/PluginSurface.tsx';
|
|
31
|
+
import { HOST_SLOTS } from '../../lib/pluginRegistry.ts';
|
|
32
|
+
import { stopSession } from '../../lib/hubApi.ts';
|
|
33
|
+
import { abbreviateCwd, runningCount, sessionStatusLine } from '../../lib/sessionSummary.ts';
|
|
34
|
+
import { DEFAULT_SETTINGS_SECTION } from '../../lib/settingsSections.ts';
|
|
35
|
+
import { paletteStore } from '../../stores/paletteStore.ts';
|
|
36
|
+
import { renameSession, sessionStoreFor } from '../../stores/sessionStore.ts';
|
|
37
|
+
import { sessionsStore, type SessionMeta } from '../../stores/sessionsStore.ts';
|
|
38
|
+
import { NewSessionDialog } from './NewSessionDialog.tsx';
|
|
39
|
+
|
|
40
|
+
const STATUS_REFRESH_MS = 30_000;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* What the card is doing besides showing its session: nothing, showing its
|
|
44
|
+
* action menu, taking a new name, or asking before a stop.
|
|
45
|
+
*/
|
|
46
|
+
type CardMode = 'view' | 'menu' | 'rename' | 'confirm';
|
|
47
|
+
|
|
48
|
+
/** The confirmation that stands between the "remove" menu item and the stop. */
|
|
49
|
+
function RemoveSessionDialog({
|
|
50
|
+
sessionId,
|
|
51
|
+
name,
|
|
52
|
+
open,
|
|
53
|
+
onConfirm,
|
|
54
|
+
onCancel,
|
|
55
|
+
}: {
|
|
56
|
+
sessionId: string;
|
|
57
|
+
name: string;
|
|
58
|
+
open: boolean;
|
|
59
|
+
onConfirm: () => void;
|
|
60
|
+
onCancel: () => void;
|
|
61
|
+
}) {
|
|
62
|
+
return (
|
|
63
|
+
<Dialog
|
|
64
|
+
open={open}
|
|
65
|
+
onOpenChange={(next) => {
|
|
66
|
+
if (!next) onCancel();
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
<DialogContent width="sm" data-testid={`session-stop-dialog-${sessionId}`} aria-describedby={undefined}>
|
|
70
|
+
<DialogHeader>
|
|
71
|
+
<DialogTitle>remove session</DialogTitle>
|
|
72
|
+
</DialogHeader>
|
|
73
|
+
<DialogBody>
|
|
74
|
+
<DialogDescription>
|
|
75
|
+
this stops <span className="font-bold text-doom-hi">{name || 'untitled'}</span> and removes it from the
|
|
76
|
+
rail. anything it is running ends now.
|
|
77
|
+
</DialogDescription>
|
|
78
|
+
<DialogFooter>
|
|
79
|
+
<Button
|
|
80
|
+
variant="outline"
|
|
81
|
+
size="md"
|
|
82
|
+
data-testid={`session-stop-cancel-${sessionId}`}
|
|
83
|
+
autoFocus
|
|
84
|
+
onClick={onCancel}
|
|
85
|
+
>
|
|
86
|
+
cancel
|
|
87
|
+
</Button>
|
|
88
|
+
<Button variant="danger" size="md" data-testid={`session-stop-confirm-${sessionId}`} onClick={onConfirm}>
|
|
89
|
+
remove
|
|
90
|
+
</Button>
|
|
91
|
+
</DialogFooter>
|
|
92
|
+
</DialogBody>
|
|
93
|
+
</DialogContent>
|
|
94
|
+
</Dialog>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function SessionCard({
|
|
99
|
+
meta,
|
|
100
|
+
ordinal,
|
|
101
|
+
active,
|
|
102
|
+
now,
|
|
103
|
+
}: {
|
|
104
|
+
meta: SessionMeta;
|
|
105
|
+
ordinal: number;
|
|
106
|
+
active: boolean;
|
|
107
|
+
now: number;
|
|
108
|
+
}) {
|
|
109
|
+
const navigate = useNavigate();
|
|
110
|
+
const summary = meta.summary;
|
|
111
|
+
const [mode, setMode] = useState<CardMode>('view');
|
|
112
|
+
const [draft, setDraft] = useState('');
|
|
113
|
+
const [error, setError] = useState('');
|
|
114
|
+
// Read when the menu closes: a choice that moved the card into another
|
|
115
|
+
// mode keeps the focus it took, rather than handing it back to the kebab.
|
|
116
|
+
const modeRef = useRef(mode);
|
|
117
|
+
modeRef.current = mode;
|
|
118
|
+
const status = sessionStatusLine(
|
|
119
|
+
{
|
|
120
|
+
attach: meta.attach,
|
|
121
|
+
phase: summary.phase,
|
|
122
|
+
phaseSince: summary.phaseSince,
|
|
123
|
+
awaitingInput: summary.awaitingInput,
|
|
124
|
+
everPrompted: summary.everPrompted,
|
|
125
|
+
lastSettledAt: summary.lastSettledAt,
|
|
126
|
+
},
|
|
127
|
+
now,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const beginRename = (): void => {
|
|
131
|
+
setDraft(summary.name);
|
|
132
|
+
setError('');
|
|
133
|
+
setMode('rename');
|
|
134
|
+
};
|
|
135
|
+
const commitRename = (): void => {
|
|
136
|
+
const name = draft.trim();
|
|
137
|
+
if (name && name !== summary.name) renameSession(name, summary.id);
|
|
138
|
+
setMode('view');
|
|
139
|
+
};
|
|
140
|
+
const stop = async (): Promise<void> => {
|
|
141
|
+
setMode('view');
|
|
142
|
+
const result = await stopSession(summary.id);
|
|
143
|
+
if ('error' in result) setError(result.error);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const cardClass = active ? 'bg-doom-selected hover:bg-doom-selected' : 'hover:bg-doom-panel';
|
|
147
|
+
const menuOpen = mode === 'menu';
|
|
148
|
+
const details = (
|
|
149
|
+
<>
|
|
150
|
+
<span
|
|
151
|
+
data-testid="session-status"
|
|
152
|
+
className={`text-[11px] leading-snug ${active ? 'line-clamp-2 text-doom-on-selected/85' : 'truncate text-doom-dim'}`}
|
|
153
|
+
>
|
|
154
|
+
{status}
|
|
155
|
+
</span>
|
|
156
|
+
{summary.git ? (
|
|
157
|
+
<span
|
|
158
|
+
data-testid="session-branch"
|
|
159
|
+
className={`flex items-center gap-[7px] pt-0.5 text-[10px] ${active ? 'text-doom-on-selected/85' : 'text-doom-faint'}`}
|
|
160
|
+
>
|
|
161
|
+
<BranchIcon
|
|
162
|
+
className={`h-[10px] w-[10px] shrink-0 ${active ? 'text-doom-on-selected/70' : 'text-doom-faint'}`}
|
|
163
|
+
/>
|
|
164
|
+
{summary.git.branch}
|
|
165
|
+
{summary.git.dirty ? '*' : ''}
|
|
166
|
+
</span>
|
|
167
|
+
) : null}
|
|
168
|
+
<span className={`truncate text-[10px] ${active ? 'text-doom-on-selected/70' : 'text-doom-faint'}`}>
|
|
169
|
+
{abbreviateCwd(summary.cwd)}
|
|
170
|
+
</span>
|
|
171
|
+
{error ? (
|
|
172
|
+
<span data-testid="session-error" className="line-clamp-3 text-[10px] break-words text-doom-red">
|
|
173
|
+
{error}
|
|
174
|
+
</span>
|
|
175
|
+
) : null}
|
|
176
|
+
</>
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<div className="group relative" data-testid={`session-card-${summary.id}`} data-active={active}>
|
|
181
|
+
{mode === 'rename' ? (
|
|
182
|
+
// The name field cannot live inside the card button, so the card
|
|
183
|
+
// briefly stops being one while it takes a name.
|
|
184
|
+
<div className={cn(buttonVariants({ variant: 'ghost', size: 'card' }), cardClass, 'cursor-default')}>
|
|
185
|
+
<Input
|
|
186
|
+
data-testid={`session-name-input-${summary.id}`}
|
|
187
|
+
value={draft}
|
|
188
|
+
autoFocus
|
|
189
|
+
onChange={(event) => setDraft(event.target.value)}
|
|
190
|
+
onKeyDown={(event) => {
|
|
191
|
+
if (event.key === 'Enter') commitRename();
|
|
192
|
+
if (event.key === 'Escape') setMode('view');
|
|
193
|
+
}}
|
|
194
|
+
onBlur={() => setMode('view')}
|
|
195
|
+
className="border-doom-blue/60 px-1.5 py-0.5 text-[13px] font-bold"
|
|
196
|
+
/>
|
|
197
|
+
{details}
|
|
198
|
+
</div>
|
|
199
|
+
) : (
|
|
200
|
+
<Button
|
|
201
|
+
variant="ghost"
|
|
202
|
+
size="card"
|
|
203
|
+
data-testid={`session-open-${summary.id}`}
|
|
204
|
+
onClick={() => void navigate({ to: '/session/$sessionId', params: { sessionId: summary.id } })}
|
|
205
|
+
className={cardClass}
|
|
206
|
+
>
|
|
207
|
+
<div className="flex items-center gap-2">
|
|
208
|
+
<span
|
|
209
|
+
className={`min-w-0 flex-1 truncate text-[13px] font-bold ${active ? 'text-doom-on-selected' : 'text-doom-hi'}`}
|
|
210
|
+
>
|
|
211
|
+
{summary.name || 'untitled'}
|
|
212
|
+
</span>
|
|
213
|
+
{ordinal <= 9 ? (
|
|
214
|
+
<span
|
|
215
|
+
title={`press ${String(ordinal)} to focus`}
|
|
216
|
+
className={`flex h-[17px] w-[17px] shrink-0 items-center justify-center rounded-full text-[9px] font-bold transition-opacity group-focus-within:opacity-0 group-hover:opacity-0 ${
|
|
217
|
+
active ? 'bg-doom-on-selected/20 text-doom-on-selected' : 'bg-doom-tint-magenta text-doom-magenta'
|
|
218
|
+
}`}
|
|
219
|
+
>
|
|
220
|
+
{ordinal}
|
|
221
|
+
</span>
|
|
222
|
+
) : null}
|
|
223
|
+
</div>
|
|
224
|
+
{details}
|
|
225
|
+
</Button>
|
|
226
|
+
)}
|
|
227
|
+
|
|
228
|
+
{mode === 'view' || menuOpen ? (
|
|
229
|
+
<div
|
|
230
|
+
className={`absolute top-2 right-2 transition-opacity ${
|
|
231
|
+
menuOpen ? 'opacity-100' : 'opacity-0 group-focus-within:opacity-100 group-hover:opacity-100'
|
|
232
|
+
}`}
|
|
233
|
+
>
|
|
234
|
+
<DropdownMenu
|
|
235
|
+
open={menuOpen}
|
|
236
|
+
onOpenChange={(next) => setMode((current) => (next ? 'menu' : current === 'menu' ? 'view' : current))}
|
|
237
|
+
>
|
|
238
|
+
<DropdownMenuTrigger asChild>
|
|
239
|
+
<Button
|
|
240
|
+
variant="ghost"
|
|
241
|
+
size="icon"
|
|
242
|
+
data-testid={`session-menu-${summary.id}`}
|
|
243
|
+
title="session actions"
|
|
244
|
+
className={
|
|
245
|
+
active
|
|
246
|
+
? 'text-doom-on-selected/80 hover:bg-doom-on-selected/20 hover:text-doom-on-selected data-[state=open]:bg-doom-on-selected/20 data-[state=open]:text-doom-on-selected'
|
|
247
|
+
: 'text-doom-faint hover:bg-doom-deep hover:text-doom-hi data-[state=open]:bg-doom-deep data-[state=open]:text-doom-hi'
|
|
248
|
+
}
|
|
249
|
+
>
|
|
250
|
+
<KebabIcon className="h-3 w-3" />
|
|
251
|
+
</Button>
|
|
252
|
+
</DropdownMenuTrigger>
|
|
253
|
+
<DropdownMenuContent
|
|
254
|
+
data-testid={`session-menu-list-${summary.id}`}
|
|
255
|
+
onCloseAutoFocus={(event) => {
|
|
256
|
+
if (modeRef.current !== 'view') event.preventDefault();
|
|
257
|
+
}}
|
|
258
|
+
>
|
|
259
|
+
<DropdownMenuItem data-testid={`session-rename-${summary.id}`} onSelect={beginRename}>
|
|
260
|
+
edit
|
|
261
|
+
</DropdownMenuItem>
|
|
262
|
+
<DropdownMenuItem
|
|
263
|
+
variant="destructive"
|
|
264
|
+
data-testid={`session-stop-${summary.id}`}
|
|
265
|
+
onSelect={() => setMode('confirm')}
|
|
266
|
+
>
|
|
267
|
+
remove
|
|
268
|
+
</DropdownMenuItem>
|
|
269
|
+
</DropdownMenuContent>
|
|
270
|
+
</DropdownMenu>
|
|
271
|
+
</div>
|
|
272
|
+
) : null}
|
|
273
|
+
<RemoveSessionDialog
|
|
274
|
+
sessionId={summary.id}
|
|
275
|
+
name={summary.name}
|
|
276
|
+
open={mode === 'confirm'}
|
|
277
|
+
onConfirm={() => void stop()}
|
|
278
|
+
onCancel={() => setMode('view')}
|
|
279
|
+
/>
|
|
280
|
+
</div>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function isEditable(target: EventTarget | null): boolean {
|
|
285
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
286
|
+
return target.isContentEditable || target.tagName === 'INPUT' || target.tagName === 'TEXTAREA';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** A key pressed inside an overlay belongs to that overlay, never to the rail. */
|
|
290
|
+
function insideOverlay(target: EventTarget | null): boolean {
|
|
291
|
+
return target instanceof Element && target.closest('[role="dialog"], [role="menu"], [role="alertdialog"]') !== null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** The mockup's rail: brand, live session cards, the new-session flow, and nothing else. */
|
|
295
|
+
export function SessionRail() {
|
|
296
|
+
const navigate = useNavigate();
|
|
297
|
+
const order = useStore(sessionsStore, (state) => state.order);
|
|
298
|
+
const byId = useStore(sessionsStore, (state) => state.byId);
|
|
299
|
+
const activeId = useStore(sessionsStore, (state) => state.activeId);
|
|
300
|
+
const hasDialog = useStore(sessionStoreFor(activeId), (state) => state.dialog !== null);
|
|
301
|
+
const [creating, setCreating] = useState(false);
|
|
302
|
+
const [now, setNow] = useState(() => Date.now());
|
|
303
|
+
|
|
304
|
+
// Keeps "running · 12m" honest without any frame arriving.
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
const timer = setInterval(() => setNow(Date.now()), STATUS_REFRESH_MS);
|
|
307
|
+
return () => clearInterval(timer);
|
|
308
|
+
}, []);
|
|
309
|
+
|
|
310
|
+
useEffect(() => {
|
|
311
|
+
const onKeyDown = (event: KeyboardEvent): void => {
|
|
312
|
+
if (event.ctrlKey && !event.metaKey && !event.altKey && event.key === 't') {
|
|
313
|
+
event.preventDefault();
|
|
314
|
+
setCreating(true);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (event.ctrlKey || event.metaKey || event.altKey || event.defaultPrevented) return;
|
|
318
|
+
if (isEditable(event.target) || insideOverlay(event.target) || paletteStore.state.open || hasDialog) return;
|
|
319
|
+
const ordinal = Number.parseInt(event.key, 10);
|
|
320
|
+
if (!Number.isInteger(ordinal) || ordinal < 1 || ordinal > 9) return;
|
|
321
|
+
const target = order[ordinal - 1];
|
|
322
|
+
if (target === undefined) return;
|
|
323
|
+
event.preventDefault();
|
|
324
|
+
void navigate({ to: '/session/$sessionId', params: { sessionId: target } });
|
|
325
|
+
};
|
|
326
|
+
window.addEventListener('keydown', onKeyDown);
|
|
327
|
+
return () => window.removeEventListener('keydown', onKeyDown);
|
|
328
|
+
}, [order, hasDialog, navigate]);
|
|
329
|
+
|
|
330
|
+
const running = runningCount(order.map((id) => byId[id].summary.phase));
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<div className="flex h-full flex-col">
|
|
334
|
+
<div className="flex items-center gap-[9px] border-b border-doom-border px-4 pt-4 pb-3.5">
|
|
335
|
+
<span className="flex items-center gap-[3px]" aria-label="DoomPi">
|
|
336
|
+
<span aria-hidden="true" className="text-[15px] font-bold tracking-[0.16em] text-doom-hi">
|
|
337
|
+
DOOM
|
|
338
|
+
</span>
|
|
339
|
+
<MascotMark size={22} />
|
|
340
|
+
</span>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
<div className="flex items-center justify-between px-4 pt-3.5 pb-2">
|
|
344
|
+
<SectionLabel>sessions</SectionLabel>
|
|
345
|
+
<span className="flex items-center gap-2.5">
|
|
346
|
+
<span data-testid="sessions-running-rail" className="text-[9px] text-doom-faint">
|
|
347
|
+
{running} running
|
|
348
|
+
</span>
|
|
349
|
+
{/* The rail's own action lives in its heading, where a heading's
|
|
350
|
+
action belongs, instead of a full-width button competing with the
|
|
351
|
+
cards for the eye. */}
|
|
352
|
+
<Button
|
|
353
|
+
variant="ghost"
|
|
354
|
+
size="icon"
|
|
355
|
+
data-testid="new-session-open"
|
|
356
|
+
title="new session"
|
|
357
|
+
aria-label="new session"
|
|
358
|
+
onClick={() => setCreating(true)}
|
|
359
|
+
className="text-doom-faint hover:text-doom-hi"
|
|
360
|
+
>
|
|
361
|
+
<PlusIcon className="h-3 w-3" />
|
|
362
|
+
</Button>
|
|
363
|
+
</span>
|
|
364
|
+
</div>
|
|
365
|
+
<div className="flex flex-col gap-1 px-2.5">
|
|
366
|
+
{order.map((id, index) => (
|
|
367
|
+
<SessionCard key={id} meta={byId[id]} ordinal={index + 1} active={id === activeId} now={now} />
|
|
368
|
+
))}
|
|
369
|
+
</div>
|
|
370
|
+
{/* With no sessions the rail has nothing to show and the plus alone is a
|
|
371
|
+
thin invitation, so the empty state spells the action out. */}
|
|
372
|
+
{order.length === 0 ? (
|
|
373
|
+
<div className="px-2.5 pt-2">
|
|
374
|
+
<Button
|
|
375
|
+
variant="outline"
|
|
376
|
+
size="lg"
|
|
377
|
+
data-testid="new-session-empty"
|
|
378
|
+
onClick={() => setCreating(true)}
|
|
379
|
+
className="w-full justify-start px-[11px] text-[11px]"
|
|
380
|
+
>
|
|
381
|
+
<PlusIcon className="h-3 w-3" />
|
|
382
|
+
new session
|
|
383
|
+
</Button>
|
|
384
|
+
</div>
|
|
385
|
+
) : null}
|
|
386
|
+
|
|
387
|
+
<PluginSurface slot={HOST_SLOTS.rail} sessionId={activeId} />
|
|
388
|
+
<div className="flex-1" />
|
|
389
|
+
<div className="flex items-center gap-2.5 border-t border-doom-border px-4 py-3">
|
|
390
|
+
<Tooltip>
|
|
391
|
+
<TooltipTrigger asChild>
|
|
392
|
+
<Button asChild variant="ghost" size="icon" className="text-doom-faint">
|
|
393
|
+
<Link
|
|
394
|
+
to="/settings/$section"
|
|
395
|
+
params={{ section: DEFAULT_SETTINGS_SECTION }}
|
|
396
|
+
data-testid="settings-open"
|
|
397
|
+
aria-label="settings"
|
|
398
|
+
>
|
|
399
|
+
<GearIcon className="h-3 w-3" />
|
|
400
|
+
</Link>
|
|
401
|
+
</Button>
|
|
402
|
+
</TooltipTrigger>
|
|
403
|
+
<TooltipContent side="top">settings</TooltipContent>
|
|
404
|
+
</Tooltip>
|
|
405
|
+
<span className="text-[9px] text-doom-faint">ctrl+k commands · ctrl+t new session</span>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
{creating ? <NewSessionDialog onClose={() => setCreating(false)} /> : null}
|
|
409
|
+
</div>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { CheckIcon, RadioGroup, RadioGroupCard } from '@agimon-ai/doompi-web-components';
|
|
2
|
+
import type { ThemeConfig } from '@agimon-ai/doompi-web-components/theme';
|
|
3
|
+
import { useStore } from '@tanstack/react-store';
|
|
4
|
+
import { availableThemes, selectTheme, themeStore } from '../../stores/themeStore.ts';
|
|
5
|
+
|
|
6
|
+
/** The accents a swatch previews, in the order the bar shows them. */
|
|
7
|
+
const SWATCH_ACCENTS = ['blue', 'green', 'yellow', 'red', 'magenta', 'cyan'] as const;
|
|
8
|
+
|
|
9
|
+
/** A miniature of the cockpit in the theme's own colours, drawn from its tokens rather than the page's. */
|
|
10
|
+
function ThemeSwatch({ theme }: { theme: ThemeConfig }) {
|
|
11
|
+
const { tokens } = theme;
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
aria-hidden
|
|
15
|
+
className="flex h-[74px] w-full overflow-hidden rounded border"
|
|
16
|
+
style={{ backgroundColor: tokens.bg, borderColor: tokens.border }}
|
|
17
|
+
>
|
|
18
|
+
<div className="flex w-[34%] flex-col gap-1 p-1.5" style={{ backgroundColor: tokens.rail }}>
|
|
19
|
+
<span className="h-[7px] w-full rounded-sm" style={{ backgroundColor: tokens.selected }} />
|
|
20
|
+
<span className="h-[7px] w-3/4 rounded-sm" style={{ backgroundColor: tokens.panel }} />
|
|
21
|
+
<span className="h-[7px] w-3/4 rounded-sm" style={{ backgroundColor: tokens.panel }} />
|
|
22
|
+
</div>
|
|
23
|
+
<div className="flex flex-1 flex-col justify-between p-1.5">
|
|
24
|
+
<div className="flex flex-col gap-1">
|
|
25
|
+
<span className="h-[5px] w-2/3 rounded-sm" style={{ backgroundColor: tokens.text }} />
|
|
26
|
+
<span className="h-[5px] w-1/2 rounded-sm" style={{ backgroundColor: tokens.dim }} />
|
|
27
|
+
</div>
|
|
28
|
+
<div className="flex gap-1">
|
|
29
|
+
{SWATCH_ACCENTS.map((accent) => (
|
|
30
|
+
<span key={accent} className="h-2 w-2 rounded-full" style={{ backgroundColor: tokens[accent] }} />
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The appearance page: the shipped themes as a picker. A choice applies at
|
|
40
|
+
* once and is remembered per browser; the token contract the themes fill
|
|
41
|
+
* is documented in the component library.
|
|
42
|
+
*/
|
|
43
|
+
export function AppearanceSettings() {
|
|
44
|
+
const current = useStore(themeStore, (state) => state.name);
|
|
45
|
+
return (
|
|
46
|
+
<div data-testid="appearance-settings" className="flex max-w-[780px] flex-col gap-4">
|
|
47
|
+
<div className="flex flex-col gap-1">
|
|
48
|
+
<h2 className="text-[13px] font-bold text-doom-hi">appearance</h2>
|
|
49
|
+
<p className="text-[11px] leading-relaxed text-doom-dim">
|
|
50
|
+
pick the theme the cockpit renders with. the choice is remembered in this browser; every colour on the page,
|
|
51
|
+
including the plugins' tabs, follows it.
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
<RadioGroup
|
|
55
|
+
aria-label="theme"
|
|
56
|
+
data-testid="theme-list"
|
|
57
|
+
className="grid grid-cols-3 gap-3"
|
|
58
|
+
value={current}
|
|
59
|
+
onValueChange={selectTheme}
|
|
60
|
+
>
|
|
61
|
+
{availableThemes().map((theme) => {
|
|
62
|
+
const selected = theme.name === current;
|
|
63
|
+
return (
|
|
64
|
+
<RadioGroupCard
|
|
65
|
+
key={theme.name}
|
|
66
|
+
value={theme.name}
|
|
67
|
+
data-testid={`theme-${theme.name}`}
|
|
68
|
+
data-selected={selected}
|
|
69
|
+
className="flex flex-col gap-2 p-2.5"
|
|
70
|
+
>
|
|
71
|
+
<ThemeSwatch theme={theme} />
|
|
72
|
+
<span className="flex items-center gap-2">
|
|
73
|
+
<span className={`flex-1 text-[12px] font-bold ${selected ? 'text-doom-blue' : 'text-doom-hi'}`}>
|
|
74
|
+
{theme.label}
|
|
75
|
+
</span>
|
|
76
|
+
<span className="text-[9px] text-doom-faint">{theme.scheme}</span>
|
|
77
|
+
{selected ? <CheckIcon className="h-3 w-3 text-doom-blue" /> : null}
|
|
78
|
+
</span>
|
|
79
|
+
</RadioGroupCard>
|
|
80
|
+
);
|
|
81
|
+
})}
|
|
82
|
+
</RadioGroup>
|
|
83
|
+
<p className="text-[10px] leading-relaxed text-doom-faint">
|
|
84
|
+
a theme is a JSON config of twenty palette tokens; see @agimon-ai/doompi-web-components for the contract.
|
|
85
|
+
</p>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|