Allow style override via postMessage (vibe-kanban) (#480)
* Commit changes from coding agent for task attempt 5ee89751-7ceb-4dfc-a9e7-0311307f9367 * Cleanup script changes for task attempt 5ee89751-7ceb-4dfc-a9e7-0311307f9367 * Commit changes from coding agent for task attempt 5ee89751-7ceb-4dfc-a9e7-0311307f9367 * Commit changes from coding agent for task attempt 5ee89751-7ceb-4dfc-a9e7-0311307f9367 * Cleanup script changes for task attempt 5ee89751-7ceb-4dfc-a9e7-0311307f9367 * Receive style overrides for VS Code * Separate style override logic * Style override * Format * Remove debug * Prettier
This commit is contained in:
committed by
GitHub
parent
605ac4a9ba
commit
b6999d1659
57
STYLE_OVERRIDE.md
Normal file
57
STYLE_OVERRIDE.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Style Override via postMessage
|
||||
|
||||
Simple API for overriding styles when embedding the frontend in an iframe.
|
||||
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
// Switch theme
|
||||
iframe.contentWindow.postMessage({
|
||||
type: 'VIBE_STYLE',
|
||||
theme: 'purple' // 'system', 'light', 'dark', 'purple', 'green', 'blue', 'orange', 'red'
|
||||
}, 'https://your-app-domain.com');
|
||||
|
||||
// Override CSS variables (--vibe-* prefix only)
|
||||
iframe.contentWindow.postMessage({
|
||||
type: 'VIBE_STYLE',
|
||||
css: {
|
||||
'--vibe-primary': '220 14% 96%',
|
||||
'--vibe-background': '0 0% 100%'
|
||||
}
|
||||
}, 'https://your-app-domain.com');
|
||||
|
||||
// Both together
|
||||
iframe.contentWindow.postMessage({
|
||||
type: 'VIBE_STYLE',
|
||||
theme: 'dark',
|
||||
css: {
|
||||
'--vibe-accent': '210 100% 50%'
|
||||
}
|
||||
}, 'https://your-app-domain.com');
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
- Origin validation via `VITE_PARENT_ORIGIN` environment variable
|
||||
- Only `--vibe-*` prefixed CSS variables can be overridden
|
||||
- Browser validates CSS values automatically
|
||||
|
||||
## Example
|
||||
|
||||
```html
|
||||
<iframe id="vibe" src="https://app.com" width="100%" height="600"></iframe>
|
||||
<script>
|
||||
const iframe = document.getElementById('vibe');
|
||||
|
||||
iframe.addEventListener('load', () => {
|
||||
// Apply custom theme
|
||||
iframe.contentWindow.postMessage({
|
||||
type: 'VIBE_STYLE',
|
||||
theme: 'purple',
|
||||
css: {
|
||||
'--vibe-brand': '210 100% 50%'
|
||||
}
|
||||
}, 'https://app.com');
|
||||
});
|
||||
</script>
|
||||
```
|
||||
@@ -67,9 +67,11 @@ async fn main() -> Result<(), VibeKanbanError> {
|
||||
let listener = tokio::net::TcpListener::bind(format!("{host}:{port}")).await?;
|
||||
let actual_port = listener.local_addr()?.port(); // get → 53427 (example)
|
||||
|
||||
// Write port file for discovery (dev builds always, release builds only if enabled)
|
||||
if cfg!(debug_assertions) || std::env::var_os("ENABLE_PORT_FILE").is_some() {
|
||||
write_port_file(actual_port).await?;
|
||||
// Write port file for discovery if prod, warn on fail
|
||||
if !cfg!(debug_assertions) {
|
||||
if let Err(e) = write_port_file(actual_port).await {
|
||||
tracing::warn!("Failed to write port file: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("Server running on http://{host}:{actual_port}");
|
||||
|
||||
@@ -18,6 +18,7 @@ import { configApi } from '@/lib/api';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Loader } from '@/components/ui/loader';
|
||||
import { GitHubLoginDialog } from '@/components/GitHubLoginDialog';
|
||||
import { AppWithStyleOverride } from '@/utils/style-override';
|
||||
|
||||
const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);
|
||||
|
||||
@@ -136,55 +137,57 @@ function AppContent() {
|
||||
|
||||
return (
|
||||
<ThemeProvider initialTheme={config?.theme || ThemeMode.SYSTEM}>
|
||||
<div className="h-screen flex flex-col bg-background">
|
||||
<GitHubLoginDialog
|
||||
open={showGitHubLogin}
|
||||
onOpenChange={handleGitHubLoginComplete}
|
||||
/>
|
||||
<DisclaimerDialog
|
||||
open={showDisclaimer}
|
||||
onAccept={handleDisclaimerAccept}
|
||||
/>
|
||||
<OnboardingDialog
|
||||
open={showOnboarding}
|
||||
onComplete={handleOnboardingComplete}
|
||||
/>
|
||||
<PrivacyOptInDialog
|
||||
open={showPrivacyOptIn}
|
||||
onComplete={handlePrivacyOptInComplete}
|
||||
/>
|
||||
{showNavbar && <Navbar />}
|
||||
<div className="flex-1 overflow-y-scroll">
|
||||
<SentryRoutes>
|
||||
<Route path="/" element={<Projects />} />
|
||||
<Route path="/projects" element={<Projects />} />
|
||||
<Route path="/projects/:projectId" element={<Projects />} />
|
||||
<Route
|
||||
path="/projects/:projectId/tasks"
|
||||
element={<ProjectTasks />}
|
||||
/>
|
||||
<Route
|
||||
path="/projects/:projectId/tasks/:taskId/full"
|
||||
element={<TaskDetailsPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/projects/:projectId/tasks/:taskId/attempts/:attemptId/full"
|
||||
element={<TaskDetailsPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/projects/:projectId/tasks/:taskId/attempts/:attemptId"
|
||||
element={<ProjectTasks />}
|
||||
/>
|
||||
<Route
|
||||
path="/projects/:projectId/tasks/:taskId"
|
||||
element={<ProjectTasks />}
|
||||
/>
|
||||
<AppWithStyleOverride>
|
||||
<div className="h-screen flex flex-col bg-background">
|
||||
<GitHubLoginDialog
|
||||
open={showGitHubLogin}
|
||||
onOpenChange={handleGitHubLoginComplete}
|
||||
/>
|
||||
<DisclaimerDialog
|
||||
open={showDisclaimer}
|
||||
onAccept={handleDisclaimerAccept}
|
||||
/>
|
||||
<OnboardingDialog
|
||||
open={showOnboarding}
|
||||
onComplete={handleOnboardingComplete}
|
||||
/>
|
||||
<PrivacyOptInDialog
|
||||
open={showPrivacyOptIn}
|
||||
onComplete={handlePrivacyOptInComplete}
|
||||
/>
|
||||
{showNavbar && <Navbar />}
|
||||
<div className="flex-1 overflow-y-scroll">
|
||||
<SentryRoutes>
|
||||
<Route path="/" element={<Projects />} />
|
||||
<Route path="/projects" element={<Projects />} />
|
||||
<Route path="/projects/:projectId" element={<Projects />} />
|
||||
<Route
|
||||
path="/projects/:projectId/tasks"
|
||||
element={<ProjectTasks />}
|
||||
/>
|
||||
<Route
|
||||
path="/projects/:projectId/tasks/:taskId/full"
|
||||
element={<TaskDetailsPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/projects/:projectId/tasks/:taskId/attempts/:attemptId/full"
|
||||
element={<TaskDetailsPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/projects/:projectId/tasks/:taskId/attempts/:attemptId"
|
||||
element={<ProjectTasks />}
|
||||
/>
|
||||
<Route
|
||||
path="/projects/:projectId/tasks/:taskId"
|
||||
element={<ProjectTasks />}
|
||||
/>
|
||||
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/mcp-servers" element={<McpServers />} />
|
||||
</SentryRoutes>
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/mcp-servers" element={<McpServers />} />
|
||||
</SentryRoutes>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppWithStyleOverride>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,219 +2,206 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* 1) THEME TOKENS (underscored): defaults + classes control these */
|
||||
@layer base {
|
||||
/* Light defaults */
|
||||
:root {
|
||||
--_background: 0 0% 100%;
|
||||
--_foreground: 222.2 84% 4.9%;
|
||||
--_card: 0 0% 100%;
|
||||
--_card-foreground: 222.2 84% 4.9%;
|
||||
--_popover: 0 0% 100%;
|
||||
--_popover-foreground: 222.2 84% 4.9%;
|
||||
--_primary: 222.2 47.4% 11.2%;
|
||||
--_primary-foreground: 210 40% 98%;
|
||||
--_secondary: 210 40% 96%;
|
||||
--_secondary-foreground: 222.2 84% 4.9%;
|
||||
--_muted: 210 40% 96%;
|
||||
--_muted-foreground: 215.4 16.3% 46.9%;
|
||||
--_accent: 210 40% 96%;
|
||||
--_accent-foreground: 222.2 84% 4.9%;
|
||||
--_destructive: 0 84.2% 60.2%;
|
||||
--_destructive-foreground: 210 40% 98%;
|
||||
--_border: 214.3 31.8% 91.4%;
|
||||
--_input: 214.3 31.8% 91.4%;
|
||||
--_ring: 222.2 84% 4.9%;
|
||||
--_radius: 0.5rem;
|
||||
|
||||
/* Status (light) */
|
||||
--_success: 142.1 76.2% 36.3%;
|
||||
--_success-foreground: 138.5 76.5% 96.7%;
|
||||
--_warning: 32.2 95% 44.1%;
|
||||
--_warning-foreground: 26 83.3% 14.1%;
|
||||
--_info: 217.2 91.2% 59.8%;
|
||||
--_info-foreground: 222.2 84% 4.9%;
|
||||
--_neutral: 210 40% 96%;
|
||||
--_neutral-foreground: 222.2 84% 4.9%;
|
||||
|
||||
/* Console (light) */
|
||||
--_console-background: 0 0% 100%;
|
||||
--_console-foreground: 222.2 84% 4.9%;
|
||||
--_console-success: 138 69% 45%;
|
||||
--_console-error: 5 100% 69%;
|
||||
}
|
||||
|
||||
/* Dark defaults (used if no theme class but user prefers dark) */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--_background: 222.2 84% 4.9%;
|
||||
--_foreground: 210 40% 98%;
|
||||
--_card: 222.2 84% 4.9%;
|
||||
--_card-foreground: 210 40% 98%;
|
||||
--_popover: 222.2 84% 4.9%;
|
||||
--_popover-foreground: 210 40% 98%;
|
||||
--_primary: 210 40% 98%;
|
||||
--_primary-foreground: 222.2 47.4% 11.2%;
|
||||
--_secondary: 217.2 32.6% 17.5%;
|
||||
--_secondary-foreground: 210 40% 98%;
|
||||
--_muted: 217.2 32.6% 17.5%;
|
||||
--_muted-foreground: 215 20.2% 65.1%;
|
||||
--_accent: 217.2 32.6% 17.5%;
|
||||
--_accent-foreground: 210 40% 98%;
|
||||
--_destructive: 0 62.8% 30.6%;
|
||||
--_destructive-foreground: 210 40% 98%;
|
||||
--_border: 217.2 32.6% 17.5%;
|
||||
--_input: 217.2 32.6% 17.5%;
|
||||
--_ring: 212.7 26.8% 83.9%;
|
||||
|
||||
/* Status (dark) */
|
||||
--_success: 138.5 76.5% 47.7%;
|
||||
--_success-foreground: 138.5 76.5% 96.7%;
|
||||
--_warning: 32.2 95% 44.1%;
|
||||
--_warning-foreground: 26 83.3% 14.1%;
|
||||
--_info: 217.2 91.2% 59.8%;
|
||||
--_info-foreground: 222.2 84% 4.9%;
|
||||
--_neutral: 217.2 32.6% 17.5%;
|
||||
--_neutral-foreground: 210 40% 98%;
|
||||
|
||||
/* Console (dark) */
|
||||
--_console-background: 0 0% 0%;
|
||||
--_console-foreground: 210 40% 98%;
|
||||
--_console-success: 138.5 76.5% 47.7%;
|
||||
--_console-error: 0 84.2% 60.2%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Your theme classes only set the UNDERSCORED tokens */
|
||||
.purple {
|
||||
--_background: 266 100% 6%;
|
||||
--_foreground: 266 20% 95%;
|
||||
--_card: 266 100% 6%;
|
||||
--_card-foreground: 266 20% 95%;
|
||||
--_popover: 266 100% 6%;
|
||||
--_popover-foreground: 266 20% 95%;
|
||||
--_primary: 266 80% 75%;
|
||||
--_primary-foreground: 266 100% 6%;
|
||||
--_secondary: 266 20% 15%;
|
||||
--_secondary-foreground: 266 20% 95%;
|
||||
--_muted: 266 20% 15%;
|
||||
--_muted-foreground: 266 15% 65%;
|
||||
--_accent: 266 20% 15%;
|
||||
--_accent-foreground: 266 20% 95%;
|
||||
--_destructive: 0 62.8% 30.6%;
|
||||
--_destructive-foreground: 266 20% 95%;
|
||||
--_border: 266 20% 15%;
|
||||
--_input: 266 20% 15%;
|
||||
--_ring: 266 80% 75%;
|
||||
|
||||
/* …status + console underscored, if you want them themed too */
|
||||
}
|
||||
|
||||
/* Repeat the same idea for .green, .blue, .orange, .red, .dark etc.,
|
||||
but ONLY set --_* tokens in these classes. */
|
||||
}
|
||||
|
||||
/* 2) PUBLIC TOKENS: prefer VS Code, else fall back to theme tokens */
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96%;
|
||||
--secondary-foreground: 222.2 84% 4.9%;
|
||||
--muted: 210 40% 96%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96%;
|
||||
--accent-foreground: 222.2 84% 4.9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--radius: 0.5rem;
|
||||
--background: var(--vscode-editor-background, var(--_background));
|
||||
--foreground: var(--vscode-editor-foreground, var(--_foreground));
|
||||
|
||||
/* Status colors */
|
||||
--success: 142.1 76.2% 36.3%;
|
||||
--success-foreground: 138.5 76.5% 96.7%;
|
||||
--warning: 32.2 95% 44.1%;
|
||||
--warning-foreground: 26 83.3% 14.1%;
|
||||
--info: 217.2 91.2% 59.8%;
|
||||
--info-foreground: 222.2 84% 4.9%;
|
||||
--neutral: 210 40% 96%;
|
||||
--neutral-foreground: 222.2 84% 4.9%;
|
||||
--card: var(--vscode-editorWidget-background, var(--_card));
|
||||
--card-foreground: var(
|
||||
--vscode-editorWidget-foreground,
|
||||
var(--_card-foreground)
|
||||
);
|
||||
--popover: var(--vscode-editorWidget-background, var(--_popover));
|
||||
--popover-foreground: var(
|
||||
--vscode-editorWidget-foreground,
|
||||
var(--_popover-foreground)
|
||||
);
|
||||
|
||||
/* Status indicator colors */
|
||||
--status-init: 210 40% 96%;
|
||||
--status-init-foreground: 222.2 84% 4.9%;
|
||||
--status-running: 217.2 91.2% 59.8%;
|
||||
--status-running-foreground: 222.2 84% 4.9%;
|
||||
--status-complete: 142.1 76.2% 36.3%;
|
||||
--status-complete-foreground: 138.5 76.5% 96.7%;
|
||||
--status-failed: 0 84.2% 60.2%;
|
||||
--status-failed-foreground: 210 40% 98%;
|
||||
--status-paused: 32.2 95% 44.1%;
|
||||
--status-paused-foreground: 26 83.3% 14.1%;
|
||||
--primary: var(--vscode-button-background, var(--_primary));
|
||||
--primary-foreground: var(
|
||||
--vscode-button-foreground,
|
||||
var(--_primary-foreground)
|
||||
);
|
||||
--secondary: var(--vscode-input-background, var(--_secondary));
|
||||
--secondary-foreground: var(
|
||||
--vscode-input-foreground,
|
||||
var(--_secondary-foreground)
|
||||
);
|
||||
|
||||
/* Console/terminal colors */
|
||||
--console-background: 222.2 84% 4.9%;
|
||||
--console-foreground: 210 40% 98%;
|
||||
--console-success: 138 69% 45%;
|
||||
--console-error: 5 100% 69%;
|
||||
}
|
||||
--muted: var(--vscode-input-background, var(--_muted));
|
||||
--muted-foreground: var(
|
||||
--vscode-descriptionForeground,
|
||||
var(--_muted-foreground)
|
||||
);
|
||||
--accent: var(--vscode-focusBorder, var(--_accent));
|
||||
--accent-foreground: var(
|
||||
--vscode-editor-foreground,
|
||||
var(--_accent-foreground)
|
||||
);
|
||||
|
||||
.purple {
|
||||
--background: 266 100% 6%;
|
||||
--foreground: 266 20% 95%;
|
||||
--card: 266 100% 6%;
|
||||
--card-foreground: 266 20% 95%;
|
||||
--popover: 266 100% 6%;
|
||||
--popover-foreground: 266 20% 95%;
|
||||
--primary: 266 80% 75%;
|
||||
--primary-foreground: 266 100% 6%;
|
||||
--secondary: 266 20% 15%;
|
||||
--secondary-foreground: 266 20% 95%;
|
||||
--muted: 266 20% 15%;
|
||||
--muted-foreground: 266 15% 65%;
|
||||
--accent: 266 20% 15%;
|
||||
--accent-foreground: 266 20% 95%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 266 20% 95%;
|
||||
--border: 266 20% 15%;
|
||||
--input: 266 20% 15%;
|
||||
--ring: 266 80% 75%;
|
||||
}
|
||||
--destructive: var(--vscode-errorForeground, var(--_destructive));
|
||||
--destructive-foreground: var(
|
||||
--vscode-button-foreground,
|
||||
var(--_destructive-foreground)
|
||||
);
|
||||
|
||||
.green {
|
||||
--background: 120 100% 6%;
|
||||
--foreground: 120 20% 95%;
|
||||
--card: 120 100% 6%;
|
||||
--card-foreground: 120 20% 95%;
|
||||
--popover: 120 100% 6%;
|
||||
--popover-foreground: 120 20% 95%;
|
||||
--primary: 120 80% 75%;
|
||||
--primary-foreground: 120 100% 6%;
|
||||
--secondary: 120 20% 15%;
|
||||
--secondary-foreground: 120 20% 95%;
|
||||
--muted: 120 20% 15%;
|
||||
--muted-foreground: 120 15% 65%;
|
||||
--accent: 120 20% 15%;
|
||||
--accent-foreground: 120 20% 95%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 120 20% 95%;
|
||||
--border: 120 20% 15%;
|
||||
--input: 120 20% 15%;
|
||||
--ring: 120 80% 75%;
|
||||
}
|
||||
--border: var(--vscode-input-background, var(--_border));
|
||||
--input: var(--vscode-input-background, var(--_input));
|
||||
--ring: var(--vscode-focusBorder, var(--_ring));
|
||||
|
||||
.blue {
|
||||
--background: 210 100% 6%;
|
||||
--foreground: 210 20% 95%;
|
||||
--card: 210 100% 6%;
|
||||
--card-foreground: 210 20% 95%;
|
||||
--popover: 210 100% 6%;
|
||||
--popover-foreground: 210 20% 95%;
|
||||
--primary: 210 80% 75%;
|
||||
--primary-foreground: 210 100% 6%;
|
||||
--secondary: 210 20% 15%;
|
||||
--secondary-foreground: 210 20% 95%;
|
||||
--muted: 210 20% 15%;
|
||||
--muted-foreground: 210 15% 65%;
|
||||
--accent: 210 20% 15%;
|
||||
--accent-foreground: 210 20% 95%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 20% 95%;
|
||||
--border: 210 20% 15%;
|
||||
--input: 210 20% 15%;
|
||||
--ring: 210 80% 75%;
|
||||
}
|
||||
--radius: var(--_radius);
|
||||
|
||||
.orange {
|
||||
--background: 30 100% 6%;
|
||||
--foreground: 30 20% 95%;
|
||||
--card: 30 100% 6%;
|
||||
--card-foreground: 30 20% 95%;
|
||||
--popover: 30 100% 6%;
|
||||
--popover-foreground: 30 20% 95%;
|
||||
--primary: 30 80% 75%;
|
||||
--primary-foreground: 30 100% 6%;
|
||||
--secondary: 30 20% 15%;
|
||||
--secondary-foreground: 30 20% 95%;
|
||||
--muted: 30 20% 15%;
|
||||
--muted-foreground: 30 15% 65%;
|
||||
--accent: 30 20% 15%;
|
||||
--accent-foreground: 30 20% 95%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 30 20% 95%;
|
||||
--border: 30 20% 15%;
|
||||
--input: 30 20% 15%;
|
||||
--ring: 30 80% 75%;
|
||||
}
|
||||
/* Status */
|
||||
--success: var(--vscode-testing-iconPassed, var(--_success));
|
||||
--success-foreground: var(
|
||||
--vscode-editor-foreground,
|
||||
var(--_success-foreground)
|
||||
);
|
||||
--warning: var(--vscode-testing-iconQueued, var(--_warning));
|
||||
--warning-foreground: var(
|
||||
--vscode-descriptionForeground,
|
||||
var(--_warning-foreground)
|
||||
);
|
||||
--info: var(--vscode-focusBorder, var(--_info));
|
||||
--info-foreground: var(--vscode-editor-foreground, var(--_info-foreground));
|
||||
--neutral: var(--vscode-input-background, var(--_neutral));
|
||||
--neutral-foreground: var(
|
||||
--vscode-editor-foreground,
|
||||
var(--_neutral-foreground)
|
||||
);
|
||||
|
||||
.red {
|
||||
--background: 0 100% 6%;
|
||||
--foreground: 0 20% 95%;
|
||||
--card: 0 100% 6%;
|
||||
--card-foreground: 0 20% 95%;
|
||||
--popover: 0 100% 6%;
|
||||
--popover-foreground: 0 20% 95%;
|
||||
--primary: 0 80% 75%;
|
||||
--primary-foreground: 0 100% 6%;
|
||||
--secondary: 0 20% 15%;
|
||||
--secondary-foreground: 0 20% 95%;
|
||||
--muted: 0 20% 15%;
|
||||
--muted-foreground: 0 15% 65%;
|
||||
--accent: 0 20% 15%;
|
||||
--accent-foreground: 0 20% 95%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 20% 95%;
|
||||
--border: 0 20% 15%;
|
||||
--input: 0 20% 15%;
|
||||
--ring: 0 80% 75%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
|
||||
/* Status colors */
|
||||
--success: 138.5 76.5% 47.7%;
|
||||
--success-foreground: 138.5 76.5% 96.7%;
|
||||
--warning: 32.2 95% 44.1%;
|
||||
--warning-foreground: 26 83.3% 14.1%;
|
||||
--info: 217.2 91.2% 59.8%;
|
||||
--info-foreground: 222.2 84% 4.9%;
|
||||
--neutral: 217.2 32.6% 17.5%;
|
||||
--neutral-foreground: 210 40% 98%;
|
||||
|
||||
/* Status indicator colors */
|
||||
--status-init: 217.2 32.6% 17.5%;
|
||||
--status-init-foreground: 210 40% 98%;
|
||||
--status-running: 217.2 91.2% 59.8%;
|
||||
--status-running-foreground: 222.2 84% 4.9%;
|
||||
--status-complete: 138.5 76.5% 47.7%;
|
||||
--status-complete-foreground: 138.5 76.5% 96.7%;
|
||||
--status-failed: 0 62.8% 30.6%;
|
||||
--status-failed-foreground: 210 40% 98%;
|
||||
--status-paused: 32.2 95% 44.1%;
|
||||
--status-paused-foreground: 26 83.3% 14.1%;
|
||||
|
||||
/* Console/terminal colors */
|
||||
--console-background: 0 0% 0%;
|
||||
--console-foreground: 138.5 76.5% 47.7%;
|
||||
--console-success: 138.5 76.5% 47.7%;
|
||||
--console-error: 0 84.2% 60.2%;
|
||||
/* Console/terminal */
|
||||
--console-background: var(
|
||||
--vscode-editor-background,
|
||||
var(--_console-background)
|
||||
);
|
||||
--console-foreground: var(
|
||||
--vscode-terminal-foreground,
|
||||
var(--_console-foreground)
|
||||
);
|
||||
--console-success: var(
|
||||
--vscode-testing-iconPassed,
|
||||
var(--_console-success)
|
||||
);
|
||||
--console-error: var(--vscode-terminal-ansiRed, var(--_console-error));
|
||||
}
|
||||
}
|
||||
|
||||
/* 3) Usage */
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
|
||||
82
frontend/src/utils/style-override.tsx
Normal file
82
frontend/src/utils/style-override.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { ThemeMode } from 'shared/types';
|
||||
|
||||
interface VibeStyleOverrideMessage {
|
||||
type: 'VIBE_STYLE_OVERRIDE';
|
||||
payload:
|
||||
| {
|
||||
kind: 'cssVars';
|
||||
variables: Record<string, string>;
|
||||
}
|
||||
| {
|
||||
kind: 'theme';
|
||||
theme: ThemeMode;
|
||||
};
|
||||
}
|
||||
|
||||
interface VibeIframeReadyMessage {
|
||||
type: 'VIBE_IFRAME_READY';
|
||||
}
|
||||
|
||||
// Component that adds postMessage listener for style overrides
|
||||
export function AppWithStyleOverride({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
function handleStyleMessage(event: MessageEvent) {
|
||||
if (event.data?.type !== 'VIBE_STYLE_OVERRIDE') return;
|
||||
|
||||
// Origin validation (only if VITE_PARENT_ORIGIN is configured)
|
||||
const allowedOrigin = import.meta.env.VITE_PARENT_ORIGIN;
|
||||
if (allowedOrigin && event.origin !== allowedOrigin) {
|
||||
console.warn(
|
||||
'[StyleOverride] Message from unauthorized origin:',
|
||||
event.origin
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const message = event.data as VibeStyleOverrideMessage;
|
||||
|
||||
// CSS variable overrides (only --vibe-* prefixed variables)
|
||||
if (
|
||||
message.payload.kind === 'cssVars' &&
|
||||
typeof message.payload.variables === 'object'
|
||||
) {
|
||||
Object.entries(message.payload.variables).forEach(([name, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
document.documentElement.style.setProperty(name, value);
|
||||
}
|
||||
});
|
||||
} else if (message.payload.kind === 'theme') {
|
||||
setTheme(message.payload.theme);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleStyleMessage);
|
||||
return () => window.removeEventListener('message', handleStyleMessage);
|
||||
}, [setTheme]);
|
||||
|
||||
// Send ready message to parent when component mounts
|
||||
useEffect(() => {
|
||||
const allowedOrigin = import.meta.env.VITE_PARENT_ORIGIN;
|
||||
|
||||
// Only send if we're in an iframe and have a parent
|
||||
if (window.parent && window.parent !== window) {
|
||||
const readyMessage: VibeIframeReadyMessage = {
|
||||
type: 'VIBE_IFRAME_READY',
|
||||
};
|
||||
|
||||
// Send to specific origin if configured, otherwise send to any origin
|
||||
const targetOrigin = allowedOrigin || '*';
|
||||
window.parent.postMessage(readyMessage, targetOrigin);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
Reference in New Issue
Block a user