Files FIles Files
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import { useState } from 'react';
|
||||
import { Copy, Check, Play } from 'lucide-react';
|
||||
|
||||
type CommandBlockProps = {
|
||||
label?: string;
|
||||
command: string;
|
||||
onRun?: (command: string) => void;
|
||||
};
|
||||
|
||||
export const CommandBlock = ({ label, command, onRun }: CommandBlockProps) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copy = () => {
|
||||
navigator.clipboard.writeText(command);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="inline-flex flex-col gap-1">
|
||||
{label && <span className="text-xs text-duck-dark/50">{label}</span>}
|
||||
<div className="inline-flex items-center gap-1 bg-[#1a1a2e] rounded-lg pl-3 pr-1 py-1.5">
|
||||
<code className="text-sm text-[#e0e0e0] font-mono whitespace-nowrap">{command}</code>
|
||||
{onRun && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onRun(command)}
|
||||
className="shrink-0 p-1.5 rounded hover:bg-white/10 cursor-pointer transition-colors"
|
||||
title="Run in terminal"
|
||||
>
|
||||
<Play className="h-3.5 w-3.5 text-duck-teal" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={copy}
|
||||
className="shrink-0 p-1.5 rounded hover:bg-white/10 cursor-pointer transition-colors"
|
||||
title="Copy command"
|
||||
>
|
||||
{copied ? <Check className="h-3.5 w-3.5 text-green-400" /> : <Copy className="h-3.5 w-3.5 text-white/40" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ArrowLeftRight } from 'lucide-react';
|
||||
import type { LayoutPanel, AppRegistry, PanelComponents } from './types';
|
||||
import type { ComponentType } from 'react';
|
||||
import { ArrowLeftRight, X } from 'lucide-react';
|
||||
import type { LayoutPanel, AppRegistry, PanelComponents, PanelComponentEntry } from './types';
|
||||
import { useWorkspace } from './WorkspaceContext';
|
||||
import { Card } from '../Card';
|
||||
import { AppPicker } from './AppPicker';
|
||||
@@ -32,6 +33,9 @@ type PanelContextMenuProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const isPanelEntry = (v: ComponentType | PanelComponentEntry): v is PanelComponentEntry =>
|
||||
typeof v === 'object' && v !== null && 'component' in v;
|
||||
|
||||
const PanelContextMenu = ({ panelId, hasApp, isLastPanel, onSplit, onRemove, onClearApp, children }: PanelContextMenuProps) => {
|
||||
const { swapSourceId, setSwapSourceId } = useWorkspace();
|
||||
|
||||
@@ -116,10 +120,18 @@ const SwapSourceIndicator = ({ panelId }: { panelId: string }) => {
|
||||
// };
|
||||
|
||||
export const PanelSlot = ({ panel, registry, components, interactive, isLastPanel, onSetApp, onSplit, onRemove }: PanelSlotProps) => {
|
||||
const PanelComponent = components?.[panel.id];
|
||||
const rawPanelComponent = components?.[panel.id];
|
||||
const panelEntry = rawPanelComponent && isPanelEntry(rawPanelComponent) ? rawPanelComponent : null;
|
||||
const PanelComponent = panelEntry ? panelEntry.component : (rawPanelComponent as ComponentType | undefined);
|
||||
|
||||
const entry = panel.appType ? registry[panel.appType] : null;
|
||||
const AppComponent = PanelComponent ?? entry?.component;
|
||||
|
||||
// Resolve header, provider, onClose from PanelComponentEntry or registry
|
||||
const HeaderComponent = panelEntry?.header ?? entry?.header;
|
||||
const ProviderComponent = panelEntry?.provider ?? entry?.provider;
|
||||
const onClose = panelEntry?.onClose;
|
||||
|
||||
const contextMenu = interactive
|
||||
? (content: React.ReactNode) => (
|
||||
<PanelContextMenu panelId={panel.id} hasApp={!!AppComponent} isLastPanel={isLastPanel} onSplit={onSplit} onRemove={onRemove} onClearApp={() => onSetApp(panel.id, null)}>
|
||||
@@ -168,6 +180,56 @@ export const PanelSlot = ({ panel, registry, components, interactive, isLastPane
|
||||
);
|
||||
}
|
||||
|
||||
// App with header — render header chrome + body
|
||||
if (HeaderComponent) {
|
||||
const headerBar = (
|
||||
<div className="shrink-0 flex items-center gap-2 px-3 py-1.5 border-b border-black/10 text-black font-semibold">
|
||||
<HeaderComponent panelId={panel.id} />
|
||||
{onClose && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 rounded hover:bg-black/10 transition-colors cursor-pointer"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const body = (
|
||||
<div className="flex-1 min-h-0">
|
||||
<Card className="h-full w-full overflow-hidden p-0 rounded-none border-0 shadow-none">
|
||||
<AppComponent panelId={panel.id} />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
const inner = ProviderComponent ? (
|
||||
<ProviderComponent panelId={panel.id}>
|
||||
{headerBar}
|
||||
{body}
|
||||
</ProviderComponent>
|
||||
) : (
|
||||
<>
|
||||
{headerBar}
|
||||
{body}
|
||||
</>
|
||||
);
|
||||
|
||||
return contextMenu(
|
||||
<div data-panel-id={panel.id} className="group/panel relative h-full w-full p-1">
|
||||
<div
|
||||
className="relative h-full w-full overflow-hidden rounded-lg border backdrop-blur-xl p-2 flex flex-col"
|
||||
style={{ backgroundColor: 'rgba(255, 255, 255, 0.12)', borderColor: 'rgba(255, 255, 255, 0.2)' }}
|
||||
>
|
||||
{inner}
|
||||
</div>
|
||||
{overlays}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
// Default: no header
|
||||
return contextMenu(
|
||||
<div data-panel-id={panel.id} className="group/panel relative h-full w-full p-1">
|
||||
<div
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type { LayoutNode, LayoutGroup, LayoutPanel, WorkspaceDefinition, AppRegistry, AppRegistryEntry, PanelComponents } from './types';
|
||||
export type { LayoutNode, LayoutGroup, LayoutPanel, WorkspaceDefinition, AppRegistry, AppRegistryEntry, PanelComponents, PanelComponentEntry } from './types';
|
||||
export type { DropPosition } from './layout-utils';
|
||||
export { createDefaultLayout, splitPanel, removePanel, setApp, updateSizes, swapPanels, movePanel, pruneEmptyPanels, countPanels, hasAnyApp } from './layout-utils';
|
||||
export { WorkspaceProvider, useWorkspace } from './WorkspaceContext';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ComponentType } from 'react';
|
||||
import type { ComponentType, ReactNode } from 'react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
|
||||
export type LayoutGroup = {
|
||||
@@ -28,6 +28,8 @@ export type AppRegistryEntry = {
|
||||
name: string;
|
||||
icon: LucideIcon;
|
||||
component: ComponentType<{ panelId: string }>;
|
||||
header?: ComponentType<{ panelId: string }>;
|
||||
provider?: ComponentType<{ panelId: string; children: ReactNode }>;
|
||||
transparent?: boolean;
|
||||
fixedHeight?: number;
|
||||
widget?: boolean;
|
||||
@@ -35,4 +37,11 @@ export type AppRegistryEntry = {
|
||||
|
||||
export type AppRegistry = Record<string, AppRegistryEntry>;
|
||||
|
||||
export type PanelComponents = Record<string, ComponentType>;
|
||||
export type PanelComponentEntry = {
|
||||
component: ComponentType;
|
||||
header?: ComponentType;
|
||||
provider?: ComponentType<{ children: ReactNode }>;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
export type PanelComponents = Record<string, ComponentType | PanelComponentEntry>;
|
||||
|
||||
Reference in New Issue
Block a user