deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
37
packages/design-panel/package.json
Normal file
37
packages/design-panel/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@theia/design-panel",
|
||||
"version": "1.68.0",
|
||||
"description": "Vibn Design Panel — screen browser and live preview",
|
||||
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
|
||||
"keywords": [
|
||||
"theia-extension"
|
||||
],
|
||||
"theiaExtensions": [
|
||||
{
|
||||
"frontend": "lib/browser/design-panel-frontend-module"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@theia/core": "1.68.0",
|
||||
"@theia/filesystem": "1.68.0",
|
||||
"@theia/workspace": "1.68.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@theia/ext-scripts": "1.68.0"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "theiaext build",
|
||||
"clean": "theiaext clean",
|
||||
"compile": "theiaext compile",
|
||||
"lint": "theiaext lint",
|
||||
"watch": "theiaext watch"
|
||||
},
|
||||
"nyc": {
|
||||
"extends": "../../configs/nyc.json"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { AbstractViewContribution } from '@theia/core/lib/browser';
|
||||
import { Command, CommandRegistry, MenuModelRegistry } from '@theia/core/lib/common';
|
||||
import { CommonMenus } from '@theia/core/lib/browser';
|
||||
import { DesignPanelWidget } from './design-panel-widget';
|
||||
|
||||
export const DesignPanelCommand: Command = {
|
||||
id: 'vibn.design.panel.open',
|
||||
label: 'Open Design Panel',
|
||||
category: 'Design',
|
||||
};
|
||||
|
||||
@injectable()
|
||||
export class DesignPanelContribution extends AbstractViewContribution<DesignPanelWidget> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: DesignPanelWidget.ID,
|
||||
widgetName: DesignPanelWidget.LABEL,
|
||||
defaultWidgetOptions: {
|
||||
area: 'main',
|
||||
},
|
||||
toggleCommandId: DesignPanelCommand.id,
|
||||
});
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
super.registerCommands(registry);
|
||||
registry.registerCommand(DesignPanelCommand, {
|
||||
execute: () => this.openView({ reveal: true, activate: true }),
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(menus: MenuModelRegistry): void {
|
||||
super.registerMenus(menus);
|
||||
menus.registerMenuAction(CommonMenus.VIEW_VIEWS, {
|
||||
commandId: DesignPanelCommand.id,
|
||||
label: 'Design Panel',
|
||||
order: 'z',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
|
||||
import { WidgetFactory, bindViewContribution } from '@theia/core/lib/browser';
|
||||
import { DesignPanelContribution } from './design-panel-contribution';
|
||||
import { DesignPanelWidget } from './design-panel-widget';
|
||||
|
||||
export default new ContainerModule((bind: interfaces.Bind) => {
|
||||
bindViewContribution(bind, DesignPanelContribution);
|
||||
|
||||
bind(DesignPanelWidget).toSelf();
|
||||
bind(WidgetFactory).toDynamicValue(ctx => ({
|
||||
id: DesignPanelWidget.ID,
|
||||
createWidget: () => ctx.container.get<DesignPanelWidget>(DesignPanelWidget),
|
||||
})).inSingletonScope();
|
||||
});
|
||||
225
packages/design-panel/src/browser/design-panel-widget.tsx
Normal file
225
packages/design-panel/src/browser/design-panel-widget.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { ReactWidget, Message } from '@theia/core/lib/browser';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import '../../src/browser/style/index.css';
|
||||
|
||||
interface Route {
|
||||
label: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface DesignPanelState {
|
||||
routes: Route[];
|
||||
selectedRoute: string | null;
|
||||
previewUrl: string;
|
||||
scanning: boolean;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class DesignPanelWidget extends ReactWidget {
|
||||
|
||||
static readonly ID = 'vibn.design.panel';
|
||||
static readonly LABEL = 'Design';
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(FileService)
|
||||
protected readonly fileService: FileService;
|
||||
|
||||
protected state: DesignPanelState = {
|
||||
routes: [],
|
||||
selectedRoute: null,
|
||||
previewUrl: 'http://localhost:3000',
|
||||
scanning: false,
|
||||
};
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.id = DesignPanelWidget.ID;
|
||||
this.title.label = DesignPanelWidget.LABEL;
|
||||
this.title.caption = 'Design — screen browser & live preview';
|
||||
this.title.iconClass = 'codicon codicon-browser';
|
||||
this.title.closable = true;
|
||||
this.node.tabIndex = 0;
|
||||
|
||||
this.workspaceService.onWorkspaceChanged(() => this.scanRoutes());
|
||||
this.workspaceService.ready.then(() => this.scanRoutes());
|
||||
}
|
||||
|
||||
protected override onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
this.node.focus();
|
||||
}
|
||||
|
||||
protected setState(partial: Partial<DesignPanelState>): void {
|
||||
this.state = { ...this.state, ...partial };
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected async scanRoutes(): Promise<void> {
|
||||
this.setState({ scanning: true, routes: [] });
|
||||
|
||||
const roots = await this.workspaceService.roots;
|
||||
if (!roots.length) {
|
||||
this.setState({ scanning: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const routes: Route[] = [];
|
||||
|
||||
for (const root of roots) {
|
||||
const appDir = root.resource.resolve('app');
|
||||
try {
|
||||
await this.collectRoutes(appDir, '', routes);
|
||||
} catch {
|
||||
// app/ dir may not exist yet
|
||||
}
|
||||
}
|
||||
|
||||
routes.sort((a, b) => a.path.localeCompare(b.path));
|
||||
this.setState({ scanning: false, routes });
|
||||
}
|
||||
|
||||
protected async collectRoutes(dir: URI, routePrefix: string, result: Route[]): Promise<void> {
|
||||
let stat;
|
||||
try {
|
||||
stat = await this.fileService.resolve(dir);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stat.isDirectory || !stat.children) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const child of stat.children) {
|
||||
// Skip private folders like (auth), _components, etc.
|
||||
const name = child.name;
|
||||
if (name.startsWith('_') || name.startsWith('.')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (child.isDirectory) {
|
||||
// Route groups like (auth) — strip parens for routing, keep for display
|
||||
const isRouteGroup = name.startsWith('(') && name.endsWith(')');
|
||||
const segment = isRouteGroup ? '' : '/' + name;
|
||||
await this.collectRoutes(child.resource, routePrefix + segment, result);
|
||||
} else if (child.name === 'page.tsx' || child.name === 'page.ts' || child.name === 'page.jsx' || child.name === 'page.js') {
|
||||
const routePath = routePrefix || '/';
|
||||
result.push({
|
||||
label: routePath,
|
||||
path: routePath,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected handleRouteClick(route: Route): void {
|
||||
const base = this.state.previewUrl.replace(/\/$/, '');
|
||||
const path = route.path === '/' ? '' : route.path;
|
||||
this.setState({ selectedRoute: route.path, previewUrl: base + path });
|
||||
}
|
||||
|
||||
protected handleUrlChange(e: React.ChangeEvent<HTMLInputElement>): void {
|
||||
this.setState({ previewUrl: e.target.value });
|
||||
}
|
||||
|
||||
protected handleUrlKeyDown(e: React.KeyboardEvent<HTMLInputElement>): void {
|
||||
if (e.key === 'Enter') {
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
protected handleRefresh(): void {
|
||||
// Force iframe reload by briefly blanking the src
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected render(): React.ReactNode {
|
||||
const { routes, selectedRoute, previewUrl, scanning } = this.state;
|
||||
|
||||
return (
|
||||
<div className="vibn-design-panel">
|
||||
{/* Top bar */}
|
||||
<div className="vibn-design-toolbar">
|
||||
<span className="vibn-design-toolbar__title">Design</span>
|
||||
<input
|
||||
className="vibn-design-url-bar"
|
||||
type="text"
|
||||
value={previewUrl}
|
||||
onChange={this.handleUrlChange.bind(this)}
|
||||
onKeyDown={this.handleUrlKeyDown.bind(this)}
|
||||
placeholder="http://localhost:3000"
|
||||
spellCheck={false}
|
||||
/>
|
||||
<button
|
||||
className="vibn-design-btn"
|
||||
title="Refresh preview"
|
||||
onClick={this.handleRefresh.bind(this)}
|
||||
>
|
||||
<span className="codicon codicon-refresh" />
|
||||
</button>
|
||||
<button
|
||||
className="vibn-design-btn"
|
||||
title="Scan routes"
|
||||
onClick={() => this.scanRoutes()}
|
||||
>
|
||||
<span className="codicon codicon-search" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="vibn-design-body">
|
||||
{/* Screen list */}
|
||||
<div className="vibn-design-screens">
|
||||
<div className="vibn-design-screens__header">
|
||||
Screens
|
||||
{scanning && <span className="vibn-design-spinner" />}
|
||||
</div>
|
||||
|
||||
{!scanning && routes.length === 0 && (
|
||||
<div className="vibn-design-screens__empty">
|
||||
<span className="codicon codicon-info" />
|
||||
<p>No Next.js routes found.</p>
|
||||
<p>Open a project with an <code>app/</code> directory.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{routes.map(route => (
|
||||
<div
|
||||
key={route.path}
|
||||
className={`vibn-design-route${selectedRoute === route.path ? ' vibn-design-route--active' : ''}`}
|
||||
onClick={() => this.handleRouteClick(route)}
|
||||
title={route.path}
|
||||
>
|
||||
<span className="codicon codicon-file-code" />
|
||||
<span className="vibn-design-route__label">{route.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
<div className="vibn-design-preview">
|
||||
{previewUrl ? (
|
||||
<iframe
|
||||
className="vibn-design-iframe"
|
||||
src={previewUrl}
|
||||
title="Design preview"
|
||||
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
|
||||
/>
|
||||
) : (
|
||||
<div className="vibn-design-preview__placeholder">
|
||||
<span className="codicon codicon-browser" />
|
||||
<p>Enter a URL above or start your dev server.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
213
packages/design-panel/src/browser/style/index.css
Normal file
213
packages/design-panel/src/browser/style/index.css
Normal file
@@ -0,0 +1,213 @@
|
||||
.vibn-design-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--theia-editor-background);
|
||||
color: var(--theia-foreground);
|
||||
font-family: var(--theia-ui-font-family);
|
||||
font-size: var(--theia-ui-font-size1);
|
||||
}
|
||||
|
||||
/* ── Toolbar ─────────────────────────────────────────── */
|
||||
.vibn-design-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
background: var(--theia-titleBar-activeBackground);
|
||||
border-bottom: 1px solid var(--theia-widget-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.vibn-design-toolbar__title {
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--theia-foreground);
|
||||
opacity: 0.7;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.vibn-design-url-bar {
|
||||
flex: 1;
|
||||
padding: 3px 8px;
|
||||
background: var(--theia-input-background);
|
||||
color: var(--theia-input-foreground);
|
||||
border: 1px solid var(--theia-input-border);
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-family: var(--theia-ui-font-family);
|
||||
outline: none;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vibn-design-url-bar:focus {
|
||||
border-color: var(--theia-focusBorder);
|
||||
}
|
||||
|
||||
.vibn-design-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
color: var(--theia-foreground);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.vibn-design-btn:hover {
|
||||
background: var(--theia-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
/* ── Body ─────────────────────────────────────────────── */
|
||||
.vibn-design-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Screen list ─────────────────────────────────────── */
|
||||
.vibn-design-screens {
|
||||
width: 180px;
|
||||
min-width: 140px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid var(--theia-widget-border);
|
||||
overflow-y: auto;
|
||||
background: var(--theia-sideBar-background);
|
||||
}
|
||||
|
||||
.vibn-design-screens__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 10px 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--theia-sideBarTitle-foreground);
|
||||
border-bottom: 1px solid var(--theia-widget-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.vibn-design-screens__empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 4px;
|
||||
padding: 20px 12px;
|
||||
color: var(--theia-descriptionForeground);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.vibn-design-screens__empty .codicon {
|
||||
font-size: 24px;
|
||||
opacity: 0.5;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vibn-design-screens__empty p {
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.vibn-design-screens__empty code {
|
||||
background: var(--theia-textCodeBlock-background);
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.vibn-design-route {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.vibn-design-route:hover {
|
||||
background: var(--theia-list-hoverBackground);
|
||||
}
|
||||
|
||||
.vibn-design-route--active {
|
||||
background: var(--theia-list-activeSelectionBackground);
|
||||
color: var(--theia-list-activeSelectionForeground);
|
||||
}
|
||||
|
||||
.vibn-design-route__label {
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Spinner ─────────────────────────────────────────── */
|
||||
.vibn-design-spinner {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 2px solid var(--theia-foreground);
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: vibn-spin 0.6s linear infinite;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@keyframes vibn-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ── Preview pane ────────────────────────────────────── */
|
||||
.vibn-design-preview {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.vibn-design-iframe {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.vibn-design-preview__placeholder {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: var(--theia-descriptionForeground);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.vibn-design-preview__placeholder .codicon {
|
||||
font-size: 40px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.vibn-design-preview__placeholder p {
|
||||
margin: 0;
|
||||
}
|
||||
16
packages/design-panel/tsconfig.json
Normal file
16
packages/design-panel/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../configs/base.tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../core" },
|
||||
{ "path": "../filesystem" },
|
||||
{ "path": "../workspace" }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user