Files FIles Files

This commit is contained in:
2026-02-20 04:28:02 +00:00
parent 1f7eb64eb3
commit d7503ca56b
20 changed files with 2195 additions and 633 deletions
@@ -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 -1
View File
@@ -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';
+11 -2
View File
@@ -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>;