@theproductguy/create-mission-control 1.0.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 +37 -0
- package/bin/cli.js +170 -0
- package/package.json +44 -0
- package/src/template/App.tsx +28 -0
- package/src/template/agent-os/commands/create-tasks/1-get-spec-requirements.md +19 -0
- package/src/template/agent-os/commands/create-tasks/2-create-tasks-list.md +234 -0
- package/src/template/agent-os/commands/create-tasks/create-tasks.md +254 -0
- package/src/template/agent-os/commands/design-screen/design-screen.md +32 -0
- package/src/template/agent-os/commands/design-shell/design-shell.md +34 -0
- package/src/template/agent-os/commands/design-tokens/design-tokens.md +36 -0
- package/src/template/agent-os/commands/export-product/export-product.md +181 -0
- package/src/template/agent-os/commands/implement-tasks/1-determine-tasks.md +13 -0
- package/src/template/agent-os/commands/implement-tasks/2-implement-tasks.md +63 -0
- package/src/template/agent-os/commands/implement-tasks/3-verify-implementation.md +113 -0
- package/src/template/agent-os/commands/implement-tasks/implement-tasks.md +207 -0
- package/src/template/agent-os/commands/initialize-design/initialize-design.md +67 -0
- package/src/template/agent-os/commands/orchestrate-tasks/orchestrate-tasks.md +180 -0
- package/src/template/agent-os/commands/plan-product/1-product-concept.md +53 -0
- package/src/template/agent-os/commands/plan-product/2-create-mission.md +78 -0
- package/src/template/agent-os/commands/plan-product/3-create-roadmap.md +73 -0
- package/src/template/agent-os/commands/plan-product/4-create-tech-stack.md +46 -0
- package/src/template/agent-os/commands/plan-product/plan-product.md +241 -0
- package/src/template/agent-os/commands/sample-data/sample-data.md +51 -0
- package/src/template/agent-os/commands/scaffold-implementation/scaffold-implementation.md +35 -0
- package/src/template/agent-os/commands/screenshot-design/screenshot-design.md +21 -0
- package/src/template/agent-os/commands/shape-spec/1-initialize-spec.md +95 -0
- package/src/template/agent-os/commands/shape-spec/2-shape-spec.md +300 -0
- package/src/template/agent-os/commands/shape-spec/shape-spec.md +40 -0
- package/src/template/agent-os/commands/write-spec/write-spec.md +134 -0
- package/src/template/agent-os/config.yml +13 -0
- package/src/template/agent-os/product/mission.md +29 -0
- package/src/template/agent-os/product/roadmap.md +9 -0
- package/src/template/agent-os/product/tech-stack.md +14 -0
- package/src/template/agent-os/scripts/generate_docs.sh +150 -0
- package/src/template/agent-os/specs/README.md +1 -0
- package/src/template/agent-os/standards/backend/api.md +10 -0
- package/src/template/agent-os/standards/backend/migrations.md +9 -0
- package/src/template/agent-os/standards/backend/models.md +10 -0
- package/src/template/agent-os/standards/backend/queries.md +9 -0
- package/src/template/agent-os/standards/frontend/accessibility.md +10 -0
- package/src/template/agent-os/standards/frontend/components.md +11 -0
- package/src/template/agent-os/standards/frontend/css.md +7 -0
- package/src/template/agent-os/standards/frontend/responsive.md +11 -0
- package/src/template/agent-os/standards/global/coding-style.md +10 -0
- package/src/template/agent-os/standards/global/commenting.md +5 -0
- package/src/template/agent-os/standards/global/conventions.md +11 -0
- package/src/template/agent-os/standards/global/error-handling.md +9 -0
- package/src/template/agent-os/standards/global/tech-stack.md +31 -0
- package/src/template/agent-os/standards/global/validation.md +11 -0
- package/src/template/agent-os/standards/testing/test-writing.md +9 -0
- package/src/template/agent-os-ui/README.md +73 -0
- package/src/template/agent-os-ui/package.json +54 -0
- package/src/template/agent-os-ui/src/components/AgentShell.tsx +31 -0
- package/src/template/agent-os-ui/src/components/AgentSidebar.tsx +65 -0
- package/src/template/agent-os-ui/src/components/GuidanceCard.tsx +75 -0
- package/src/template/agent-os-ui/src/components/MarkdownViewer.tsx +25 -0
- package/src/template/agent-os-ui/src/components/PromptButton.tsx +28 -0
- package/src/template/agent-os-ui/src/components/StatusItem.tsx +45 -0
- package/src/template/agent-os-ui/src/components/ThemeToggle.tsx +72 -0
- package/src/template/agent-os-ui/src/index.ts +11 -0
- package/src/template/agent-os-ui/src/style.css +3 -0
- package/src/template/agent-os-ui/tsconfig.json +33 -0
- package/src/template/agent-os-ui/vite.config.ts +32 -0
- package/src/template/control-center/backend/index.js +253 -0
- package/src/template/control-center/backend/package.json +19 -0
- package/src/template/control-center/frontend/README.md +73 -0
- package/src/template/control-center/frontend/eslint.config.js +23 -0
- package/src/template/control-center/frontend/index.html +21 -0
- package/src/template/control-center/frontend/package.json +43 -0
- package/src/template/control-center/frontend/postcss.config.js +6 -0
- package/src/template/control-center/frontend/public/favicon.svg +4 -0
- package/src/template/control-center/frontend/public/runtime-config.json +11 -0
- package/src/template/control-center/frontend/public/vite.svg +1 -0
- package/src/template/control-center/frontend/src/App.css +42 -0
- package/src/template/control-center/frontend/src/App.tsx +835 -0
- package/src/template/control-center/frontend/src/assets/react.svg +1 -0
- package/src/template/control-center/frontend/src/components/ThemeToggle.tsx +64 -0
- package/src/template/control-center/frontend/src/components/ui/ToastContext.tsx +81 -0
- package/src/template/control-center/frontend/src/index.css +149 -0
- package/src/template/control-center/frontend/src/main.tsx +14 -0
- package/src/template/control-center/frontend/src/vite-env.d.ts +1 -0
- package/src/template/control-center/frontend/tailwind.config.js +81 -0
- package/src/template/control-center/frontend/tsconfig.app.json +28 -0
- package/src/template/control-center/frontend/tsconfig.json +7 -0
- package/src/template/control-center/frontend/tsconfig.node.json +26 -0
- package/src/template/control-center/frontend/vite.config.ts +21 -0
- package/src/template/design/.claude/commands/design-os/data-model.md +122 -0
- package/src/template/design/.claude/commands/design-os/design-screen.md +309 -0
- package/src/template/design/.claude/commands/design-os/design-shell.md +238 -0
- package/src/template/design/.claude/commands/design-os/design-tokens.md +166 -0
- package/src/template/design/.claude/commands/design-os/export-product.md +1105 -0
- package/src/template/design/.claude/commands/design-os/product-roadmap.md +121 -0
- package/src/template/design/.claude/commands/design-os/product-vision.md +99 -0
- package/src/template/design/.claude/commands/design-os/sample-data.md +263 -0
- package/src/template/design/.claude/commands/design-os/screenshot-design.md +112 -0
- package/src/template/design/.claude/commands/design-os/shape-section.md +138 -0
- package/src/template/design/.claude/skills/frontend-design/SKILL.md +42 -0
- package/src/template/design/.github/CODE_OF_CONDUCT.md +5 -0
- package/src/template/design/.github/CONTRIBUTING.md +51 -0
- package/src/template/design/.github/ISSUE_TEMPLATE/config.yml +22 -0
- package/src/template/design/.github/PULL_REQUEST_TEMPLATE.md +20 -0
- package/src/template/design/.github/SECURITY.yml +5 -0
- package/src/template/design/.github/SUPPORT.md +19 -0
- package/src/template/design/.github/workflows/pr-decline.yml +135 -0
- package/src/template/design/.github/workflows/stale.yml +25 -0
- package/src/template/design/CHANGELOG.md +13 -0
- package/src/template/design/LICENSE +21 -0
- package/src/template/design/README.md +54 -0
- package/src/template/design/agents.md +218 -0
- package/src/template/design/claude.md +1 -0
- package/src/template/design/components.json +22 -0
- package/src/template/design/docs/codebase-implementation.md +153 -0
- package/src/template/design/docs/design-section.md +135 -0
- package/src/template/design/docs/export.md +149 -0
- package/src/template/design/docs/getting-started.md +59 -0
- package/src/template/design/docs/index.md +56 -0
- package/src/template/design/docs/product-planning.md +113 -0
- package/src/template/design/docs/requirements.md +22 -0
- package/src/template/design/docs/usage.md +62 -0
- package/src/template/design/eslint.config.js +23 -0
- package/src/template/design/index.html +21 -0
- package/src/template/design/package.json +46 -0
- package/src/template/design/postcss.config.js +6 -0
- package/src/template/design/public/favicon.svg +4 -0
- package/src/template/design/public/vite.svg +1 -0
- package/src/template/design/src/assets/react.svg +1 -0
- package/src/template/design/src/components/AppLayout.tsx +95 -0
- package/src/template/design/src/components/DataCard.tsx +139 -0
- package/src/template/design/src/components/DataModelPage.tsx +120 -0
- package/src/template/design/src/components/DesignPage.tsx +284 -0
- package/src/template/design/src/components/EmptyState.tsx +158 -0
- package/src/template/design/src/components/ExportPage.tsx +354 -0
- package/src/template/design/src/components/NextPhaseButton.tsx +33 -0
- package/src/template/design/src/components/PhaseNav.tsx +152 -0
- package/src/template/design/src/components/PhaseWarningBanner.tsx +81 -0
- package/src/template/design/src/components/ProductOverviewCard.tsx +102 -0
- package/src/template/design/src/components/ProductPage.tsx +97 -0
- package/src/template/design/src/components/ScreenDesignPage.tsx +370 -0
- package/src/template/design/src/components/ScreenDesignsCard.tsx +49 -0
- package/src/template/design/src/components/SectionPage.tsx +256 -0
- package/src/template/design/src/components/SectionsCard.tsx +47 -0
- package/src/template/design/src/components/SectionsPage.tsx +181 -0
- package/src/template/design/src/components/ShellCard.tsx +85 -0
- package/src/template/design/src/components/ShellDesignPage.tsx +242 -0
- package/src/template/design/src/components/SpecCard.tsx +121 -0
- package/src/template/design/src/components/StepIndicator.tsx +75 -0
- package/src/template/design/src/components/ThemeToggle.tsx +86 -0
- package/src/template/design/src/components/ui/ToastContext.tsx +81 -0
- package/src/template/design/src/components/ui/avatar.tsx +53 -0
- package/src/template/design/src/components/ui/badge.tsx +46 -0
- package/src/template/design/src/components/ui/button.tsx +60 -0
- package/src/template/design/src/components/ui/card.tsx +92 -0
- package/src/template/design/src/components/ui/collapsible.tsx +48 -0
- package/src/template/design/src/components/ui/dialog.tsx +143 -0
- package/src/template/design/src/components/ui/dropdown-menu.tsx +255 -0
- package/src/template/design/src/components/ui/input.tsx +21 -0
- package/src/template/design/src/components/ui/label.tsx +22 -0
- package/src/template/design/src/components/ui/progress.tsx +24 -0
- package/src/template/design/src/components/ui/scroll-area.tsx +18 -0
- package/src/template/design/src/components/ui/select.tsx +67 -0
- package/src/template/design/src/components/ui/separator.tsx +28 -0
- package/src/template/design/src/components/ui/sheet.tsx +137 -0
- package/src/template/design/src/components/ui/skeleton.tsx +13 -0
- package/src/template/design/src/components/ui/switch.tsx +46 -0
- package/src/template/design/src/components/ui/table.tsx +116 -0
- package/src/template/design/src/components/ui/tabs.tsx +64 -0
- package/src/template/design/src/index.css +239 -0
- package/src/template/design/src/lib/data-model-loader.ts +91 -0
- package/src/template/design/src/lib/design-system-loader.ts +101 -0
- package/src/template/design/src/lib/product-loader.ts +221 -0
- package/src/template/design/src/lib/router.tsx +61 -0
- package/src/template/design/src/lib/section-loader.ts +272 -0
- package/src/template/design/src/lib/shell-loader.ts +175 -0
- package/src/template/design/src/lib/utils.ts +6 -0
- package/src/template/design/src/main.tsx +15 -0
- package/src/template/design/src/sections/.gitkeep +0 -0
- package/src/template/design/src/sections/ai-orchestration-engine-oai/OrchestrationEngine.tsx +348 -0
- package/src/template/design/src/sections/core-platform-shell/AppShell.tsx +403 -0
- package/src/template/design/src/sections/gemini-live-integration/GeminiIntegration.tsx +332 -0
- package/src/template/design/src/sections/interactive-2d-canvas/WhiteboardCanvas.tsx +334 -0
- package/src/template/design/src/sections/participation-equity-tracker/EquityTracker.tsx +383 -0
- package/src/template/design/src/sections/persistent-memory-system/PersistentMemory.tsx +308 -0
- package/src/template/design/src/sections/real-time-communication-layer/VideoSession.tsx +342 -0
- package/src/template/design/src/sections/visual-intelligence-agents/VisualAgents.tsx +311 -0
- package/src/template/design/src/types/product.ts +97 -0
- package/src/template/design/src/types/section.ts +33 -0
- package/src/template/design/tailwind.config.js +77 -0
- package/src/template/design/tsconfig.app.json +34 -0
- package/src/template/design/tsconfig.json +13 -0
- package/src/template/design/tsconfig.node.json +26 -0
- package/src/template/design/vite.config.ts +17 -0
- package/src/template/index.css +102 -0
- package/src/template/package.json +27 -0
- package/src/template/tailwind.config.js +80 -0
- package/src/template/vite.config.ts +9 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { Moon, Sun } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
export type Theme = 'light' | 'dark' | 'system'
|
|
5
|
+
|
|
6
|
+
interface ThemeToggleProps {
|
|
7
|
+
onThemeChange?: (theme: Theme) => void
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ThemeToggle({ onThemeChange, className }: ThemeToggleProps) {
|
|
12
|
+
const [theme, setTheme] = useState<Theme>(() => {
|
|
13
|
+
if (typeof window !== 'undefined') {
|
|
14
|
+
return (localStorage.getItem('theme') as Theme) || 'system'
|
|
15
|
+
}
|
|
16
|
+
return 'system'
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const root = document.documentElement
|
|
21
|
+
|
|
22
|
+
const applyTheme = (targetTheme: Theme) => {
|
|
23
|
+
if (targetTheme === 'system') {
|
|
24
|
+
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
25
|
+
root.classList.toggle('dark', systemDark)
|
|
26
|
+
} else {
|
|
27
|
+
root.classList.toggle('dark', targetTheme === 'dark')
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
applyTheme(theme)
|
|
32
|
+
localStorage.setItem('theme', theme)
|
|
33
|
+
|
|
34
|
+
// Notify parent
|
|
35
|
+
onThemeChange?.(theme)
|
|
36
|
+
|
|
37
|
+
// Listen for system theme changes when in system mode
|
|
38
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
|
39
|
+
const handleChange = () => {
|
|
40
|
+
if (theme === 'system') {
|
|
41
|
+
applyTheme('system')
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
mediaQuery.addEventListener('change', handleChange)
|
|
45
|
+
|
|
46
|
+
return () => mediaQuery.removeEventListener('change', handleChange)
|
|
47
|
+
}, [theme, onThemeChange])
|
|
48
|
+
|
|
49
|
+
const toggleTheme = () => {
|
|
50
|
+
setTheme((prev) => {
|
|
51
|
+
if (prev === 'light') return 'dark'
|
|
52
|
+
if (prev === 'dark') return 'system'
|
|
53
|
+
return 'light'
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const isDark = theme === 'dark' || (theme === 'system' && typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<button
|
|
61
|
+
onClick={toggleTheme}
|
|
62
|
+
className={`w-9 h-9 flex items-center justify-center rounded-md text-stone-500 hover:text-stone-900 dark:text-stone-400 dark:hover:text-stone-100 hover:bg-stone-100 dark:hover:bg-stone-800 transition-colors ${className || ''}`}
|
|
63
|
+
title={`Theme: ${theme}`}
|
|
64
|
+
>
|
|
65
|
+
{isDark ? (
|
|
66
|
+
<Moon className="w-5 h-5" strokeWidth={1.5} />
|
|
67
|
+
) : (
|
|
68
|
+
<Sun className="w-5 h-5" strokeWidth={1.5} />
|
|
69
|
+
)}
|
|
70
|
+
</button>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import './style.css'
|
|
2
|
+
|
|
3
|
+
export { AgentShell } from './components/AgentShell'
|
|
4
|
+
export { AgentSidebar } from './components/AgentSidebar'
|
|
5
|
+
export { GuidanceCard } from './components/GuidanceCard'
|
|
6
|
+
export { ThemeToggle } from './components/ThemeToggle'
|
|
7
|
+
export { PromptButton } from './components/PromptButton'
|
|
8
|
+
export { StatusItem } from './components/StatusItem'
|
|
9
|
+
export { MarkdownViewer } from './components/MarkdownViewer'
|
|
10
|
+
// Export types if needed
|
|
11
|
+
export type { Theme } from './components/ThemeToggle'
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": [
|
|
6
|
+
"ES2020",
|
|
7
|
+
"DOM",
|
|
8
|
+
"DOM.Iterable"
|
|
9
|
+
],
|
|
10
|
+
"module": "ESNext",
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"jsx": "react-jsx",
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true,
|
|
22
|
+
"declaration": true,
|
|
23
|
+
"baseUrl": ".",
|
|
24
|
+
"paths": {
|
|
25
|
+
"@/*": [
|
|
26
|
+
"./src/*"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"include": [
|
|
31
|
+
"src"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import dts from 'vite-plugin-dts';
|
|
4
|
+
import { resolve } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = resolve(__filename, '..');
|
|
9
|
+
|
|
10
|
+
export default defineConfig({
|
|
11
|
+
plugins: [
|
|
12
|
+
react(),
|
|
13
|
+
dts({ include: ['src'] })
|
|
14
|
+
],
|
|
15
|
+
build: {
|
|
16
|
+
lib: {
|
|
17
|
+
entry: resolve(__dirname, 'src/index.ts'),
|
|
18
|
+
name: 'AgentOSUI',
|
|
19
|
+
fileName: 'agent-os-ui',
|
|
20
|
+
},
|
|
21
|
+
rollupOptions: {
|
|
22
|
+
external: ['react', 'react-dom', 'react/jsx-runtime'],
|
|
23
|
+
output: {
|
|
24
|
+
globals: {
|
|
25
|
+
react: 'React',
|
|
26
|
+
'react-dom': 'ReactDOM',
|
|
27
|
+
'react/jsx-runtime': 'jsxRuntime',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const cors = require('cors');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { glob } = require('glob');
|
|
6
|
+
|
|
7
|
+
const app = express();
|
|
8
|
+
const PORT = process.env.PORT || 5403;
|
|
9
|
+
const DESIGN_PORT = process.env.DESIGN_PORT || 5400;
|
|
10
|
+
const APP_PORT = process.env.APP_PORT || 5402;
|
|
11
|
+
|
|
12
|
+
// Configuration
|
|
13
|
+
const PROJECT_ROOT = path.resolve(__dirname, '../..'); // learning-agent-os root
|
|
14
|
+
const APP_DIR = path.join(PROJECT_ROOT, 'app');
|
|
15
|
+
const AGENT_OS_DIR = path.join(PROJECT_ROOT, 'agent-os');
|
|
16
|
+
|
|
17
|
+
app.use(cors());
|
|
18
|
+
app.use(express.json());
|
|
19
|
+
|
|
20
|
+
// Helper to check file existence
|
|
21
|
+
const checkFile = (filePath) => {
|
|
22
|
+
return fs.existsSync(path.join(AGENT_OS_DIR, filePath));
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Helper to parse markdown checkboxes
|
|
26
|
+
const parseStatus = (relativePath) => {
|
|
27
|
+
const fullPath = path.join(AGENT_OS_DIR, relativePath);
|
|
28
|
+
console.log(`Reading path: ${fullPath}`);
|
|
29
|
+
if (!fs.existsSync(fullPath)) {
|
|
30
|
+
console.log(`File not found: ${fullPath}`);
|
|
31
|
+
return { exists: false, completed: 0, total: 0, nextItem: null, items: [] };
|
|
32
|
+
}
|
|
33
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
34
|
+
const total = (content.match(/- \[ \]/g) || []).length + (content.match(/- \[x\]/g) || []).length;
|
|
35
|
+
const completed = (content.match(/- \[x\]/g) || []).length;
|
|
36
|
+
|
|
37
|
+
// Find next incomplete item and all items
|
|
38
|
+
let nextItem = null;
|
|
39
|
+
const items = [];
|
|
40
|
+
const lines = content.split('\n');
|
|
41
|
+
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
const match = line.match(/^\s*-\s*\[([ x])\]\s*(.+)$/);
|
|
44
|
+
if (match) {
|
|
45
|
+
const isCompleted = match[1] === 'x';
|
|
46
|
+
const text = match[2].trim();
|
|
47
|
+
// Clean up text (remove bolding, etc)
|
|
48
|
+
const cleanText = text.replace(/\*\*/g, '').replace(/^\d+\.\s*/, '');
|
|
49
|
+
|
|
50
|
+
items.push({ name: cleanText, completed: isCompleted });
|
|
51
|
+
|
|
52
|
+
if (!nextItem && !isCompleted) {
|
|
53
|
+
nextItem = cleanText;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check for boilerplate indicators
|
|
59
|
+
const boilerplateMarkers = [
|
|
60
|
+
'[Your Product Name]',
|
|
61
|
+
'[Database choice]',
|
|
62
|
+
'[Role]',
|
|
63
|
+
'[Feature 1]',
|
|
64
|
+
'[Target Audience]',
|
|
65
|
+
'[Unique Value Proposition',
|
|
66
|
+
'MVP Launch',
|
|
67
|
+
'First User',
|
|
68
|
+
'Feature A'
|
|
69
|
+
];
|
|
70
|
+
const isBoilerplate = boilerplateMarkers.some(marker => content.includes(marker));
|
|
71
|
+
|
|
72
|
+
return { exists: true, isBoilerplate, completed, total, nextItem, items };
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Helper to check service health
|
|
76
|
+
const checkService = (port) => {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const http = require('http');
|
|
79
|
+
const req = http.get(`http://localhost:${port}/`, { timeout: 2000 }, (res) => {
|
|
80
|
+
// If we get any response, the port is active and serving HTTP
|
|
81
|
+
resolve(true);
|
|
82
|
+
// conn.destroy(); // Not needed if we just consume/ignore.
|
|
83
|
+
// Actually, we should consume resume to avoid hanging?
|
|
84
|
+
// Or just destroy.
|
|
85
|
+
res.resume();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
req.on('error', (e) => {
|
|
89
|
+
resolve(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
req.on('timeout', () => {
|
|
93
|
+
req.destroy();
|
|
94
|
+
resolve(false);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// API: Get Project Status
|
|
100
|
+
app.get('/api/status', async (req, res) => {
|
|
101
|
+
try {
|
|
102
|
+
const productFiles = {
|
|
103
|
+
mission: parseStatus('product/mission.md').exists ? parseStatus('product/mission.md') : parseStatus('product/product-overview.md'),
|
|
104
|
+
roadmap: parseStatus('product/roadmap.md').exists ? parseStatus('product/roadmap.md') : parseStatus('product/product-roadmap.md'),
|
|
105
|
+
techStack: parseStatus('product/tech-stack.md'),
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Get Specs
|
|
109
|
+
const specDirs = glob.sync('specs/*/', { cwd: AGENT_OS_DIR });
|
|
110
|
+
const specs = specDirs.map(dir => {
|
|
111
|
+
const name = path.basename(dir);
|
|
112
|
+
return {
|
|
113
|
+
name,
|
|
114
|
+
spec: parseStatus(`specs/${name}/spec.md`),
|
|
115
|
+
tasks: parseStatus(`specs/${name}/tasks.md`),
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Check Services
|
|
120
|
+
const services = {
|
|
121
|
+
api: true, // We are responding, so we are alive
|
|
122
|
+
design: await checkService(DESIGN_PORT),
|
|
123
|
+
app: await checkService(APP_PORT)
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Check for Design OS export and steps
|
|
127
|
+
const designDir = path.join(PROJECT_ROOT, 'design-system');
|
|
128
|
+
// Note: In dev, these files are in design/public/product. In prod, injected elsewhere.
|
|
129
|
+
// But for this local dev setup, we check where the initialize-design command puts them?
|
|
130
|
+
// Actually, initialize-design puts them in `design/public/product`.
|
|
131
|
+
|
|
132
|
+
const hasDesignExport = fs.existsSync(path.join(PROJECT_ROOT, 'product-plan'));
|
|
133
|
+
const hasDesignInit = fs.existsSync(path.join(designDir, 'product/product-overview.md'));
|
|
134
|
+
const hasTokens = fs.existsSync(path.join(designDir, 'product/design-system/colors.json'));
|
|
135
|
+
const hasShell = fs.existsSync(path.join(designDir, 'product/shell/spec.md'));
|
|
136
|
+
|
|
137
|
+
res.json({
|
|
138
|
+
product: productFiles,
|
|
139
|
+
services,
|
|
140
|
+
design: {
|
|
141
|
+
exists: hasTokens,
|
|
142
|
+
initialized: hasDesignInit || fs.existsSync(path.join(designDir, 'product/mission.md')) || fs.existsSync(path.join(designDir, 'product/product-overview.md')),
|
|
143
|
+
tokens: hasTokens,
|
|
144
|
+
shell: hasShell,
|
|
145
|
+
exported: hasDesignExport,
|
|
146
|
+
exportPrompts: {
|
|
147
|
+
oneShot: fs.existsSync(path.join(PROJECT_ROOT, 'product-plan/prompts/one-shot-prompt.md')),
|
|
148
|
+
section: fs.existsSync(path.join(PROJECT_ROOT, 'product-plan/prompts/section-prompt.md'))
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
implementation: {
|
|
152
|
+
scaffolded: fs.existsSync(path.join(APP_DIR, 'src/lib/utils.ts'))
|
|
153
|
+
},
|
|
154
|
+
specs,
|
|
155
|
+
projectRoot: PROJECT_ROOT
|
|
156
|
+
});
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error(error);
|
|
159
|
+
res.status(500).json({ error: error.message });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// API: Scaffold New Spec
|
|
164
|
+
app.post('/api/scaffold/spec', (req, res) => {
|
|
165
|
+
const { name } = req.body;
|
|
166
|
+
if (!name) return res.status(400).json({ error: 'Spec name required' });
|
|
167
|
+
|
|
168
|
+
const specDir = path.join(AGENT_OS_DIR, 'specs', name);
|
|
169
|
+
if (fs.existsSync(specDir)) return res.status(400).json({ error: 'Spec already exists' });
|
|
170
|
+
|
|
171
|
+
fs.mkdirSync(specDir, { recursive: true });
|
|
172
|
+
// Create empty placeholders
|
|
173
|
+
fs.writeFileSync(path.join(specDir, 'spec.md'), '# Spec: ' + name);
|
|
174
|
+
fs.writeFileSync(path.join(specDir, 'tasks.md'), '# Tasks\n\n- [ ] Initial task');
|
|
175
|
+
|
|
176
|
+
res.json({ success: true, path: specDir });
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// API: Delete Spec
|
|
180
|
+
app.delete('/api/scaffold/spec/:name', (req, res) => {
|
|
181
|
+
const { name } = req.params;
|
|
182
|
+
if (!name) return res.status(400).json({ error: 'Spec name required' });
|
|
183
|
+
|
|
184
|
+
// Validate name to prevent directory traversal or deleting non-spec files
|
|
185
|
+
// Allow alphanumeric, hyphens, underscores
|
|
186
|
+
if (!/^[a-zA-Z0-9-_]+$/.test(name)) {
|
|
187
|
+
return res.status(400).json({ error: 'Invalid spec name' });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const specDir = path.join(AGENT_OS_DIR, 'specs', name);
|
|
191
|
+
if (!fs.existsSync(specDir)) {
|
|
192
|
+
return res.status(404).json({ error: 'Spec not found' });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
fs.rmSync(specDir, { recursive: true, force: true });
|
|
197
|
+
res.json({ success: true });
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error('Delete error:', error);
|
|
200
|
+
res.status(500).json({ error: 'Failed to delete spec' });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// API: Get File Content
|
|
205
|
+
app.get('/api/files', (req, res) => {
|
|
206
|
+
const { path: filePath } = req.query;
|
|
207
|
+
if (!filePath) return res.status(400).json({ error: 'File path required' });
|
|
208
|
+
|
|
209
|
+
// Prevent directory traversal
|
|
210
|
+
const safePath = path.normalize(filePath).replace(/^(\.\.(\/|\\|$))+/, '');
|
|
211
|
+
const absolutePath = path.join(AGENT_OS_DIR, safePath);
|
|
212
|
+
|
|
213
|
+
if (!absolutePath.startsWith(AGENT_OS_DIR)) {
|
|
214
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!fs.existsSync(absolutePath)) {
|
|
218
|
+
return res.status(404).json({ error: 'File not found' });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
223
|
+
res.json({ content });
|
|
224
|
+
} catch (error) {
|
|
225
|
+
res.status(500).json({ error: error.message });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// API: Save File Content
|
|
230
|
+
app.post('/api/files', (req, res) => {
|
|
231
|
+
const { path: filePath, content } = req.body;
|
|
232
|
+
if (!filePath || content === undefined) return res.status(400).json({ error: 'File path and content required' });
|
|
233
|
+
|
|
234
|
+
// Prevent directory traversal
|
|
235
|
+
const safePath = path.normalize(filePath).replace(/^(\.\.(\/|\\|$))+/, '');
|
|
236
|
+
const absolutePath = path.join(AGENT_OS_DIR, safePath);
|
|
237
|
+
|
|
238
|
+
if (!absolutePath.startsWith(AGENT_OS_DIR)) {
|
|
239
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
fs.writeFileSync(absolutePath, content, 'utf-8');
|
|
244
|
+
res.json({ success: true });
|
|
245
|
+
} catch (error) {
|
|
246
|
+
res.status(500).json({ error: error.message });
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
app.listen(PORT, () => {
|
|
251
|
+
console.log(`Control Center Backend running on http://localhost:${PORT}`);
|
|
252
|
+
console.log(`Monitoring Agent OS at: ${AGENT_OS_DIR}`);
|
|
253
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "backend",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node index.js",
|
|
8
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [],
|
|
11
|
+
"author": "",
|
|
12
|
+
"license": "ISC",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"cors": "^2.8.5",
|
|
15
|
+
"dotenv": "^17.2.3",
|
|
16
|
+
"express": "^5.2.1",
|
|
17
|
+
"glob": "^10.3.10"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# React + TypeScript + Vite
|
|
2
|
+
|
|
3
|
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
4
|
+
|
|
5
|
+
Currently, two official plugins are available:
|
|
6
|
+
|
|
7
|
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
|
8
|
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
9
|
+
|
|
10
|
+
## React Compiler
|
|
11
|
+
|
|
12
|
+
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
|
13
|
+
|
|
14
|
+
## Expanding the ESLint configuration
|
|
15
|
+
|
|
16
|
+
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
export default defineConfig([
|
|
20
|
+
globalIgnores(['dist']),
|
|
21
|
+
{
|
|
22
|
+
files: ['**/*.{ts,tsx}'],
|
|
23
|
+
extends: [
|
|
24
|
+
// Other configs...
|
|
25
|
+
|
|
26
|
+
// Remove tseslint.configs.recommended and replace with this
|
|
27
|
+
tseslint.configs.recommendedTypeChecked,
|
|
28
|
+
// Alternatively, use this for stricter rules
|
|
29
|
+
tseslint.configs.strictTypeChecked,
|
|
30
|
+
// Optionally, add this for stylistic rules
|
|
31
|
+
tseslint.configs.stylisticTypeChecked,
|
|
32
|
+
|
|
33
|
+
// Other configs...
|
|
34
|
+
],
|
|
35
|
+
languageOptions: {
|
|
36
|
+
parserOptions: {
|
|
37
|
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
38
|
+
tsconfigRootDir: import.meta.dirname,
|
|
39
|
+
},
|
|
40
|
+
// other options...
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
])
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
// eslint.config.js
|
|
50
|
+
import reactX from 'eslint-plugin-react-x'
|
|
51
|
+
import reactDom from 'eslint-plugin-react-dom'
|
|
52
|
+
|
|
53
|
+
export default defineConfig([
|
|
54
|
+
globalIgnores(['dist']),
|
|
55
|
+
{
|
|
56
|
+
files: ['**/*.{ts,tsx}'],
|
|
57
|
+
extends: [
|
|
58
|
+
// Other configs...
|
|
59
|
+
// Enable lint rules for React
|
|
60
|
+
reactX.configs['recommended-typescript'],
|
|
61
|
+
// Enable lint rules for React DOM
|
|
62
|
+
reactDom.configs.recommended,
|
|
63
|
+
],
|
|
64
|
+
languageOptions: {
|
|
65
|
+
parserOptions: {
|
|
66
|
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
67
|
+
tsconfigRootDir: import.meta.dirname,
|
|
68
|
+
},
|
|
69
|
+
// other options...
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
])
|
|
73
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import globals from 'globals'
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
5
|
+
import tseslint from 'typescript-eslint'
|
|
6
|
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
7
|
+
|
|
8
|
+
export default defineConfig([
|
|
9
|
+
globalIgnores(['dist']),
|
|
10
|
+
{
|
|
11
|
+
files: ['**/*.{ts,tsx}'],
|
|
12
|
+
extends: [
|
|
13
|
+
js.configs.recommended,
|
|
14
|
+
tseslint.configs.recommended,
|
|
15
|
+
reactHooks.configs.flat.recommended,
|
|
16
|
+
reactRefresh.configs.vite,
|
|
17
|
+
],
|
|
18
|
+
languageOptions: {
|
|
19
|
+
ecmaVersion: 2020,
|
|
20
|
+
globals: globals.browser,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
])
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link
|
|
10
|
+
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=IBM+Plex+Mono:wght@400;500&display=swap"
|
|
11
|
+
rel="stylesheet">
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
13
|
+
<title>Control Center</title>
|
|
14
|
+
</head>
|
|
15
|
+
|
|
16
|
+
<body>
|
|
17
|
+
<div id="root"></div>
|
|
18
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
19
|
+
</body>
|
|
20
|
+
|
|
21
|
+
</html>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "frontend",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"lint": "eslint .",
|
|
10
|
+
"preview": "vite preview"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@builderos/agent-os-ui": "*",
|
|
14
|
+
"@tailwindcss/typography": "^0.5.19",
|
|
15
|
+
"axios": "^1.13.2",
|
|
16
|
+
"clsx": "^2.1.1",
|
|
17
|
+
"framer-motion": "^12.23.26",
|
|
18
|
+
"lucide-react": "^0.562.0",
|
|
19
|
+
"react": "^18.3.1",
|
|
20
|
+
"react-dom": "^18.3.1",
|
|
21
|
+
"react-markdown": "^9.0.1",
|
|
22
|
+
"remark-breaks": "^4.0.0",
|
|
23
|
+
"remark-gfm": "^4.0.1",
|
|
24
|
+
"tailwind-merge": "^3.4.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@eslint/js": "^9.39.1",
|
|
28
|
+
"@types/node": "^24.10.1",
|
|
29
|
+
"@types/react": "^18.3.12",
|
|
30
|
+
"@types/react-dom": "^18.3.1",
|
|
31
|
+
"@vitejs/plugin-react": "^5.1.1",
|
|
32
|
+
"autoprefixer": "^10.4.23",
|
|
33
|
+
"eslint": "^9.39.1",
|
|
34
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
35
|
+
"eslint-plugin-react-refresh": "^0.4.24",
|
|
36
|
+
"globals": "^16.5.0",
|
|
37
|
+
"postcss": "^8.5.6",
|
|
38
|
+
"tailwindcss": "^3.4.17",
|
|
39
|
+
"typescript": "~5.9.3",
|
|
40
|
+
"typescript-eslint": "^8.46.4",
|
|
41
|
+
"vite": "^7.2.4"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#root {
|
|
2
|
+
max-width: 1280px;
|
|
3
|
+
margin: 0 auto;
|
|
4
|
+
padding: 2rem;
|
|
5
|
+
text-align: center;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.logo {
|
|
9
|
+
height: 6em;
|
|
10
|
+
padding: 1.5em;
|
|
11
|
+
will-change: filter;
|
|
12
|
+
transition: filter 300ms;
|
|
13
|
+
}
|
|
14
|
+
.logo:hover {
|
|
15
|
+
filter: drop-shadow(0 0 2em #646cffaa);
|
|
16
|
+
}
|
|
17
|
+
.logo.react:hover {
|
|
18
|
+
filter: drop-shadow(0 0 2em #61dafbaa);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@keyframes logo-spin {
|
|
22
|
+
from {
|
|
23
|
+
transform: rotate(0deg);
|
|
24
|
+
}
|
|
25
|
+
to {
|
|
26
|
+
transform: rotate(360deg);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
31
|
+
a:nth-of-type(2) .logo {
|
|
32
|
+
animation: logo-spin infinite 20s linear;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.card {
|
|
37
|
+
padding: 2em;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.read-the-docs {
|
|
41
|
+
color: #888;
|
|
42
|
+
}
|