refactor: reorganize CSS and Tool Workshop page
- Extract styles.css into styles/ directory (tokens, global, utilities, syntax-highlight, pages) - Extract ToolWorkshopPage into components: - ToolTypeListSidebar, ToolTypeEditorPanel, ToolWorkshopMobileView - use-tool-workshop hook for state management - Slim ToolWorkshopPage from 1,269 to 110 lines Quality gates: tsc --noEmit passes, npm run build passes
This commit is contained in:
@@ -0,0 +1,373 @@
|
|||||||
|
import { Icon } from "../../icon";
|
||||||
|
import { ManifestEditor } from "../tool/manifest-editor";
|
||||||
|
import type { ToolType } from "../../../api/tool-types";
|
||||||
|
import type { ToolDefinitionManifest } from "../../../api/tool-definitions";
|
||||||
|
|
||||||
|
export interface ToolTypeFormState {
|
||||||
|
name: string;
|
||||||
|
display_name: string;
|
||||||
|
description: string;
|
||||||
|
category: string;
|
||||||
|
interface_type: "web" | "terminal";
|
||||||
|
requires_port: boolean;
|
||||||
|
default_port: string;
|
||||||
|
definition_type: "compose" | "dockerfile" | "manifest";
|
||||||
|
compose_template: string;
|
||||||
|
dockerfile_template: string;
|
||||||
|
readiness_command: string;
|
||||||
|
readiness_timeout: string;
|
||||||
|
readiness_interval: string;
|
||||||
|
required_variables: string;
|
||||||
|
startup_command: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolTypeEditorPanelProps {
|
||||||
|
isCreating: boolean;
|
||||||
|
selectedToolType: ToolType | null;
|
||||||
|
form: ToolTypeFormState;
|
||||||
|
manifestData: Record<string, unknown> | null;
|
||||||
|
manifestDefinitionId: string | null;
|
||||||
|
baseDefinitions: ToolDefinitionManifest[];
|
||||||
|
toolTypeError: string | null;
|
||||||
|
toolTypeDirty: boolean;
|
||||||
|
onFormChange: (changes: Partial<ToolTypeFormState>) => void;
|
||||||
|
onManifestChange: (manifest: Record<string, unknown> | null) => void;
|
||||||
|
onSubmit: (e: React.FormEvent) => void;
|
||||||
|
onReset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToolTypeEditorPanel = ({
|
||||||
|
isCreating,
|
||||||
|
selectedToolType,
|
||||||
|
form,
|
||||||
|
manifestData,
|
||||||
|
manifestDefinitionId,
|
||||||
|
baseDefinitions,
|
||||||
|
toolTypeError,
|
||||||
|
toolTypeDirty,
|
||||||
|
onFormChange,
|
||||||
|
onManifestChange,
|
||||||
|
onSubmit,
|
||||||
|
onReset,
|
||||||
|
}: ToolTypeEditorPanelProps) => {
|
||||||
|
const hasSelection = isCreating || selectedToolType;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ flex: 1, overflow: "auto", padding: "1.5rem", minWidth: 0 }}>
|
||||||
|
{!hasSelection ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
textAlign: "center",
|
||||||
|
paddingTop: "4rem",
|
||||||
|
color: "var(--muted)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ opacity: 0.3, marginBottom: "1rem" }}>
|
||||||
|
<Icon name="code" size="lg" />
|
||||||
|
</div>
|
||||||
|
<h3 style={{ margin: "0 0 0.5rem 0", fontWeight: 500 }}>
|
||||||
|
Select a tool type
|
||||||
|
</h3>
|
||||||
|
<p style={{ margin: 0 }}>
|
||||||
|
Choose a tool from the list to edit, or create a new one.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<div style={{ marginBottom: "1.5rem" }}>
|
||||||
|
<h1 style={{ margin: "0 0 0.5rem 0", fontSize: "1.5rem" }}>
|
||||||
|
{isCreating
|
||||||
|
? "Create Tool Type"
|
||||||
|
: selectedToolType?.display_name}
|
||||||
|
</h1>
|
||||||
|
{!isCreating && (
|
||||||
|
<p className="muted" style={{ margin: 0 }}>
|
||||||
|
{selectedToolType?.name} · {selectedToolType?.definition_type}{" "}
|
||||||
|
·{" "}
|
||||||
|
{selectedToolType?.interface_type === "web"
|
||||||
|
? `Port ${selectedToolType?.default_port}`
|
||||||
|
: "Terminal"}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
className="stack"
|
||||||
|
style={{ gap: "1rem", maxWidth: "800px" }}
|
||||||
|
>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="definition-type">Definition Type</label>
|
||||||
|
<select
|
||||||
|
id="definition-type"
|
||||||
|
value={form.definition_type}
|
||||||
|
onChange={(e) => {
|
||||||
|
onFormChange({
|
||||||
|
definition_type: e.target.value as
|
||||||
|
| "compose"
|
||||||
|
| "dockerfile"
|
||||||
|
| "manifest",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="form-input"
|
||||||
|
disabled={!isCreating}
|
||||||
|
>
|
||||||
|
<option value="compose">Docker Compose</option>
|
||||||
|
<option value="dockerfile">Dockerfile</option>
|
||||||
|
<option value="manifest">Manifest (Declarative)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row" style={{ gap: "1rem" }}>
|
||||||
|
<div className="form-group" style={{ flex: 1 }}>
|
||||||
|
<label htmlFor="tool-type-name">Name *</label>
|
||||||
|
<input
|
||||||
|
id="tool-type-name"
|
||||||
|
type="text"
|
||||||
|
value={form.name}
|
||||||
|
onChange={(e) => {
|
||||||
|
onFormChange({ name: e.target.value });
|
||||||
|
}}
|
||||||
|
disabled={!isCreating}
|
||||||
|
placeholder="e.g., code-server"
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group" style={{ flex: 1 }}>
|
||||||
|
<label htmlFor="tool-type-display-name">
|
||||||
|
Display Name *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="tool-type-display-name"
|
||||||
|
type="text"
|
||||||
|
value={form.display_name}
|
||||||
|
onChange={(e) => {
|
||||||
|
onFormChange({ display_name: e.target.value });
|
||||||
|
}}
|
||||||
|
placeholder="e.g., VS Code Server"
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="tool-type-description">Description</label>
|
||||||
|
<input
|
||||||
|
id="tool-type-description"
|
||||||
|
type="text"
|
||||||
|
value={form.description}
|
||||||
|
onChange={(e) => {
|
||||||
|
onFormChange({ description: e.target.value });
|
||||||
|
}}
|
||||||
|
placeholder="Optional description"
|
||||||
|
className="form-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row" style={{ gap: "1rem" }}>
|
||||||
|
<div className="form-group" style={{ flex: 1 }}>
|
||||||
|
<label htmlFor="tool-type-category">Category</label>
|
||||||
|
<input
|
||||||
|
id="tool-type-category"
|
||||||
|
type="text"
|
||||||
|
value={form.category}
|
||||||
|
onChange={(e) => {
|
||||||
|
onFormChange({ category: e.target.value });
|
||||||
|
}}
|
||||||
|
placeholder="e.g., editor, notebook, ai-assistant"
|
||||||
|
className="form-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group" style={{ flex: 1 }}>
|
||||||
|
<label htmlFor="tool-type-interface">Interface Type</label>
|
||||||
|
<select
|
||||||
|
id="tool-type-interface"
|
||||||
|
value={form.interface_type}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value as "web" | "terminal";
|
||||||
|
onFormChange({
|
||||||
|
interface_type: value,
|
||||||
|
requires_port: value === "web",
|
||||||
|
default_port:
|
||||||
|
value === "web" ? form.default_port : "",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="form-input"
|
||||||
|
>
|
||||||
|
<option value="web">Web</option>
|
||||||
|
<option value="terminal">Terminal</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{form.interface_type === "terminal" && (
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="tool-type-startup-command">
|
||||||
|
Startup Command
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="tool-type-startup-command"
|
||||||
|
type="text"
|
||||||
|
value={form.startup_command}
|
||||||
|
onChange={(e) => {
|
||||||
|
onFormChange({ startup_command: e.target.value });
|
||||||
|
}}
|
||||||
|
placeholder="e.g., cd /workspace && ls"
|
||||||
|
className="form-input"
|
||||||
|
/>
|
||||||
|
<small className="form-help">
|
||||||
|
Command to run before the interactive shell for each new
|
||||||
|
terminal session.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{form.requires_port && (
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="tool-type-default-port">
|
||||||
|
Default Port *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="tool-type-default-port"
|
||||||
|
type="number"
|
||||||
|
value={form.default_port}
|
||||||
|
onChange={(e) => {
|
||||||
|
onFormChange({ default_port: e.target.value });
|
||||||
|
}}
|
||||||
|
placeholder="e.g., 8443"
|
||||||
|
className="form-input"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{form.definition_type === "manifest" ? (
|
||||||
|
<ManifestEditor
|
||||||
|
manifest={manifestData}
|
||||||
|
baseDefinitions={baseDefinitions}
|
||||||
|
onChange={(m) => {
|
||||||
|
onManifestChange(m);
|
||||||
|
}}
|
||||||
|
definitionId={manifestDefinitionId}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="tool-type-template">
|
||||||
|
{form.definition_type === "compose"
|
||||||
|
? "Compose Template"
|
||||||
|
: "Dockerfile"}{" "}
|
||||||
|
*
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="tool-type-template"
|
||||||
|
value={
|
||||||
|
form.definition_type === "compose"
|
||||||
|
? form.compose_template
|
||||||
|
: form.dockerfile_template
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (form.definition_type === "compose") {
|
||||||
|
onFormChange({ compose_template: e.target.value });
|
||||||
|
} else {
|
||||||
|
onFormChange({ dockerfile_template: e.target.value });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
rows={12}
|
||||||
|
placeholder={
|
||||||
|
form.definition_type === "compose"
|
||||||
|
? "version: '3.8'\nservices:\n app:\n image: ..."
|
||||||
|
: "FROM node:18\nWORKDIR /app\n..."
|
||||||
|
}
|
||||||
|
className="form-input"
|
||||||
|
style={{
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="readiness-command">
|
||||||
|
Readiness Probe Command
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="readiness-command"
|
||||||
|
type="text"
|
||||||
|
value={form.readiness_command}
|
||||||
|
onChange={(e) => {
|
||||||
|
onFormChange({ readiness_command: e.target.value });
|
||||||
|
}}
|
||||||
|
placeholder="e.g., curl -f http://localhost:8080"
|
||||||
|
className="form-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row" style={{ gap: "1rem" }}>
|
||||||
|
<div className="form-group" style={{ flex: 1 }}>
|
||||||
|
<label htmlFor="readiness-timeout">Timeout (seconds)</label>
|
||||||
|
<input
|
||||||
|
id="readiness-timeout"
|
||||||
|
type="number"
|
||||||
|
value={form.readiness_timeout}
|
||||||
|
onChange={(e) => {
|
||||||
|
onFormChange({ readiness_timeout: e.target.value });
|
||||||
|
}}
|
||||||
|
className="form-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group" style={{ flex: 1 }}>
|
||||||
|
<label htmlFor="readiness-interval">
|
||||||
|
Interval (seconds)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="readiness-interval"
|
||||||
|
type="number"
|
||||||
|
value={form.readiness_interval}
|
||||||
|
onChange={(e) => {
|
||||||
|
onFormChange({ readiness_interval: e.target.value });
|
||||||
|
}}
|
||||||
|
className="form-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Required Variables (comma-separated)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={form.required_variables}
|
||||||
|
onChange={(e) => {
|
||||||
|
onFormChange({ required_variables: e.target.value });
|
||||||
|
}}
|
||||||
|
placeholder="REPO_PATH, TOOL_NAME"
|
||||||
|
className="form-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{toolTypeError && <p className="text-error">{toolTypeError}</p>}
|
||||||
|
|
||||||
|
<div className="dialog-actions" style={{ marginTop: "1rem" }}>
|
||||||
|
<button type="submit">
|
||||||
|
<Icon name={isCreating ? "add" : "save"} size="sm" />
|
||||||
|
{isCreating ? "Create Tool Type" : "Save Changes"}
|
||||||
|
</button>
|
||||||
|
{toolTypeDirty && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onReset}
|
||||||
|
className="button-secondary"
|
||||||
|
>
|
||||||
|
<Icon name="cancel" size="sm" /> Discard
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { Icon } from "../../icon";
|
||||||
|
import type { ToolType } from "../../../api/tool-types";
|
||||||
|
|
||||||
|
interface ToolTypeListSidebarProps {
|
||||||
|
toolTypes: ToolType[];
|
||||||
|
selectedToolTypeId: string | null;
|
||||||
|
onSelect: (toolType: ToolType) => void;
|
||||||
|
onCreate: () => void;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToolTypeListSidebar = ({
|
||||||
|
toolTypes,
|
||||||
|
selectedToolTypeId,
|
||||||
|
onSelect,
|
||||||
|
onCreate,
|
||||||
|
onDelete,
|
||||||
|
}: ToolTypeListSidebarProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "280px",
|
||||||
|
minWidth: "280px",
|
||||||
|
borderRight: "1px solid var(--border)",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
background: "var(--panel)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{ padding: "1rem", borderBottom: "1px solid var(--border)" }}
|
||||||
|
>
|
||||||
|
<h2 style={{ margin: 0, fontSize: "1.125rem" }}>Tool Workshop</h2>
|
||||||
|
<p
|
||||||
|
className="muted"
|
||||||
|
style={{ margin: "0.25rem 0 0 0", fontSize: "0.875rem" }}
|
||||||
|
>
|
||||||
|
{toolTypes.length} tool type{toolTypes.length !== 1 ? "s" : ""}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ flex: 1, overflowY: "auto", padding: "0.5rem" }}>
|
||||||
|
{toolTypes.map((toolType) => (
|
||||||
|
<button
|
||||||
|
key={toolType.id}
|
||||||
|
onClick={() => onSelect(toolType)}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
textAlign: "left",
|
||||||
|
padding: "0.75rem 1rem",
|
||||||
|
marginBottom: "0.25rem",
|
||||||
|
borderRadius: "0.375rem",
|
||||||
|
border: "none",
|
||||||
|
background:
|
||||||
|
selectedToolTypeId === toolType.id
|
||||||
|
? "var(--brand)"
|
||||||
|
: "transparent",
|
||||||
|
color:
|
||||||
|
selectedToolTypeId === toolType.id ? "white" : "var(--ink)",
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "0.75rem",
|
||||||
|
transition: "background 0.15s",
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (selectedToolTypeId !== toolType.id) {
|
||||||
|
e.currentTarget.style.background = "#ece7df";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (selectedToolTypeId !== toolType.id) {
|
||||||
|
e.currentTarget.style.background = "transparent";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9375rem",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{toolType.display_name}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "0.8125rem",
|
||||||
|
opacity: 0.8,
|
||||||
|
marginTop: "0.125rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{toolType.category || "Uncategorized"} ·{" "}
|
||||||
|
{toolType.interface_type === "web"
|
||||||
|
? `Port ${toolType.default_port}`
|
||||||
|
: "Terminal"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDelete(toolType.id);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
color:
|
||||||
|
selectedToolTypeId === toolType.id
|
||||||
|
? "rgba(255,255,255,0.8)"
|
||||||
|
: "var(--muted)",
|
||||||
|
cursor: "pointer",
|
||||||
|
padding: "0.25rem",
|
||||||
|
borderRadius: "0.25rem",
|
||||||
|
flexShrink: 0,
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
className="delete-btn"
|
||||||
|
title="Delete tool type"
|
||||||
|
>
|
||||||
|
<Icon name="delete" size="sm" />
|
||||||
|
</button>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ padding: "1rem", borderTop: "1px solid var(--border)" }}>
|
||||||
|
<button
|
||||||
|
onClick={onCreate}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: "0.5rem",
|
||||||
|
padding: "0.75rem",
|
||||||
|
borderRadius: "0.5rem",
|
||||||
|
border: "2px dashed var(--border)",
|
||||||
|
background: "transparent",
|
||||||
|
color: "var(--muted)",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontWeight: 600,
|
||||||
|
transition: "all 0.15s",
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = "var(--brand)";
|
||||||
|
e.currentTarget.style.color = "var(--brand)";
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = "var(--border)";
|
||||||
|
e.currentTarget.style.color = "var(--muted)";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="add" size="sm" /> New Tool Type
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
import { MobileListView } from "../mobile/mobile-list-view";
|
||||||
|
import { MobileDetailView } from "../mobile/mobile-detail-view";
|
||||||
|
import { MobileEditView } from "../mobile/mobile-edit-view";
|
||||||
|
import { MobileFAB } from "../mobile/mobile-fab";
|
||||||
|
import type { ToolType } from "../../../api/tool-types";
|
||||||
|
import type { ToolTypeFormState } from "./ToolTypeEditorPanel";
|
||||||
|
|
||||||
|
export type MobileView = "list" | "detail" | "edit";
|
||||||
|
|
||||||
|
interface ToolWorkshopMobileViewProps {
|
||||||
|
toolTypes: ToolType[];
|
||||||
|
selectedToolType: ToolType | null;
|
||||||
|
mobileView: MobileView;
|
||||||
|
isCreating: boolean;
|
||||||
|
toolTypeForm: ToolTypeFormState;
|
||||||
|
toolTypeError: string | null;
|
||||||
|
onViewChange: (view: MobileView) => void;
|
||||||
|
onSelect: (toolType: ToolType) => void;
|
||||||
|
onCreate: () => void;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
|
onFormChange: (changes: Partial<ToolTypeFormState>) => void;
|
||||||
|
onSubmit: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToolWorkshopMobileView = ({
|
||||||
|
toolTypes,
|
||||||
|
selectedToolType,
|
||||||
|
mobileView,
|
||||||
|
isCreating,
|
||||||
|
toolTypeForm,
|
||||||
|
toolTypeError,
|
||||||
|
onViewChange,
|
||||||
|
onSelect,
|
||||||
|
onCreate,
|
||||||
|
onDelete,
|
||||||
|
onFormChange,
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
}: ToolWorkshopMobileViewProps) => {
|
||||||
|
if (mobileView === "list") {
|
||||||
|
return (
|
||||||
|
<div className="mobile-page">
|
||||||
|
<div className="mobile-page-header">
|
||||||
|
<h1>Tool Workshop</h1>
|
||||||
|
<span className="muted">{toolTypes.length} tool types</span>
|
||||||
|
</div>
|
||||||
|
<MobileListView
|
||||||
|
items={toolTypes.map((t) => ({
|
||||||
|
id: t.id,
|
||||||
|
title: t.display_name,
|
||||||
|
subtitle: `${t.category || "Uncategorized"} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`,
|
||||||
|
}))}
|
||||||
|
onItemClick={(id) => {
|
||||||
|
const toolType = toolTypes.find((t) => t.id === id);
|
||||||
|
if (toolType) {
|
||||||
|
onSelect(toolType);
|
||||||
|
onViewChange("detail");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
emptyMessage="No tool types yet"
|
||||||
|
/>
|
||||||
|
<MobileFAB
|
||||||
|
onClick={() => {
|
||||||
|
onCreate();
|
||||||
|
onViewChange("edit");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mobileView === "detail" && selectedToolType) {
|
||||||
|
return (
|
||||||
|
<MobileDetailView
|
||||||
|
title={selectedToolType.display_name}
|
||||||
|
subtitle={`${selectedToolType.name} · ${selectedToolType.definition_type} · ${selectedToolType.interface_type === "web" ? `Port ${selectedToolType.default_port}` : "Terminal"}`}
|
||||||
|
fields={[
|
||||||
|
{ label: "Name", value: selectedToolType.name },
|
||||||
|
{ label: "Display Name", value: selectedToolType.display_name },
|
||||||
|
{ label: "Description", value: selectedToolType.description },
|
||||||
|
{ label: "Category", value: selectedToolType.category },
|
||||||
|
{ label: "Interface Type", value: selectedToolType.interface_type },
|
||||||
|
{
|
||||||
|
label: "Requires Port",
|
||||||
|
value: selectedToolType.requires_port,
|
||||||
|
type: "boolean",
|
||||||
|
},
|
||||||
|
{ label: "Default Port", value: selectedToolType.default_port },
|
||||||
|
{
|
||||||
|
label: "Definition Type",
|
||||||
|
value: selectedToolType.definition_type,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Startup Command",
|
||||||
|
value: selectedToolType.startup_command,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Readiness Command",
|
||||||
|
value: selectedToolType.readiness_probe?.command ?? null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Readiness Timeout",
|
||||||
|
value: selectedToolType.readiness_probe?.timeout ?? null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Readiness Interval",
|
||||||
|
value: selectedToolType.readiness_probe?.interval ?? null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Required Variables",
|
||||||
|
value: selectedToolType.required_variables?.join(", ") ?? null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Compose Template",
|
||||||
|
value: selectedToolType.compose_template,
|
||||||
|
type: "code",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Dockerfile Template",
|
||||||
|
value: selectedToolType.dockerfile_template,
|
||||||
|
type: "code",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onEdit={() => {
|
||||||
|
onViewChange("edit");
|
||||||
|
}}
|
||||||
|
onDelete={() => {
|
||||||
|
void onDelete(selectedToolType.id);
|
||||||
|
onViewChange("list");
|
||||||
|
}}
|
||||||
|
onBack={() => {
|
||||||
|
onViewChange("list");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mobileView === "edit") {
|
||||||
|
return (
|
||||||
|
<MobileEditView
|
||||||
|
title={isCreating ? "Create Tool Type" : "Edit Tool Type"}
|
||||||
|
onCancel={onCancel}
|
||||||
|
onSave={() => {
|
||||||
|
onSubmit();
|
||||||
|
if (!toolTypeError) {
|
||||||
|
onViewChange("list");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isSaving={false}
|
||||||
|
>
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label">Name *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={toolTypeForm.name}
|
||||||
|
onChange={(e) => onFormChange({ name: e.target.value })}
|
||||||
|
className="mobile-form-input"
|
||||||
|
placeholder="e.g., my-tool"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label">Display Name *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={toolTypeForm.display_name}
|
||||||
|
onChange={(e) => onFormChange({ display_name: e.target.value })}
|
||||||
|
className="mobile-form-input"
|
||||||
|
placeholder="e.g., My Tool"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label">Description</label>
|
||||||
|
<textarea
|
||||||
|
value={toolTypeForm.description}
|
||||||
|
onChange={(e) => onFormChange({ description: e.target.value })}
|
||||||
|
className="mobile-form-textarea"
|
||||||
|
placeholder="What does this tool do?"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label">Category</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={toolTypeForm.category}
|
||||||
|
onChange={(e) => onFormChange({ category: e.target.value })}
|
||||||
|
className="mobile-form-input"
|
||||||
|
placeholder="e.g., development"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label">Interface Type</label>
|
||||||
|
<select
|
||||||
|
value={toolTypeForm.interface_type}
|
||||||
|
onChange={(e) =>
|
||||||
|
onFormChange({
|
||||||
|
interface_type: e.target.value as "web" | "terminal",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="mobile-form-select"
|
||||||
|
>
|
||||||
|
<option value="web">Web</option>
|
||||||
|
<option value="terminal">Terminal</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label">Requires Port</label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={toolTypeForm.requires_port}
|
||||||
|
onChange={(e) => onFormChange({ requires_port: e.target.checked })}
|
||||||
|
className="mobile-form-checkbox"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label">Default Port</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={toolTypeForm.default_port}
|
||||||
|
onChange={(e) => onFormChange({ default_port: e.target.value })}
|
||||||
|
className="mobile-form-input"
|
||||||
|
placeholder="e.g., 8080"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label">Definition Type</label>
|
||||||
|
<select
|
||||||
|
value={toolTypeForm.definition_type}
|
||||||
|
onChange={(e) =>
|
||||||
|
onFormChange({
|
||||||
|
definition_type: e.target.value as "compose" | "dockerfile",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="mobile-form-select"
|
||||||
|
>
|
||||||
|
<option value="compose">Compose</option>
|
||||||
|
<option value="dockerfile">Dockerfile</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label">Startup Command</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={toolTypeForm.startup_command}
|
||||||
|
onChange={(e) => onFormChange({ startup_command: e.target.value })}
|
||||||
|
className="mobile-form-input"
|
||||||
|
placeholder="Command to run on startup"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label">Readiness Command</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={toolTypeForm.readiness_command}
|
||||||
|
onChange={(e) => onFormChange({ readiness_command: e.target.value })}
|
||||||
|
className="mobile-form-input"
|
||||||
|
placeholder="e.g., curl -f http://localhost:8080/health"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label">Readiness Timeout</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={toolTypeForm.readiness_timeout}
|
||||||
|
onChange={(e) => onFormChange({ readiness_timeout: e.target.value })}
|
||||||
|
className="mobile-form-input"
|
||||||
|
placeholder="30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label">Readiness Interval</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={toolTypeForm.readiness_interval}
|
||||||
|
onChange={(e) => onFormChange({ readiness_interval: e.target.value })}
|
||||||
|
className="mobile-form-input"
|
||||||
|
placeholder="2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label">Required Variables</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={toolTypeForm.required_variables}
|
||||||
|
onChange={(e) => onFormChange({ required_variables: e.target.value })}
|
||||||
|
className="mobile-form-input"
|
||||||
|
placeholder="VAR1, VAR2, VAR3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{toolTypeForm.definition_type === "compose" && (
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label">Compose Template</label>
|
||||||
|
<textarea
|
||||||
|
value={toolTypeForm.compose_template}
|
||||||
|
onChange={(e) => onFormChange({ compose_template: e.target.value })}
|
||||||
|
className="mobile-form-textarea mobile-form-code"
|
||||||
|
placeholder="version: '3'"
|
||||||
|
rows={10}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{toolTypeForm.definition_type === "dockerfile" && (
|
||||||
|
<div className="mobile-form-group">
|
||||||
|
<label className="mobile-form-label">Dockerfile Template</label>
|
||||||
|
<textarea
|
||||||
|
value={toolTypeForm.dockerfile_template}
|
||||||
|
onChange={(e) =>
|
||||||
|
onFormChange({ dockerfile_template: e.target.value })
|
||||||
|
}
|
||||||
|
className="mobile-form-textarea mobile-form-code"
|
||||||
|
placeholder="FROM ubuntu:22.04"
|
||||||
|
rows={10}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</MobileEditView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mobile-page">
|
||||||
|
<div className="mobile-page-header">
|
||||||
|
<h1>Tool Workshop</h1>
|
||||||
|
<span className="muted">{toolTypes.length} tool types</span>
|
||||||
|
</div>
|
||||||
|
<MobileListView
|
||||||
|
items={toolTypes.map((t) => ({
|
||||||
|
id: t.id,
|
||||||
|
title: t.display_name,
|
||||||
|
subtitle: `${t.category || "Uncategorized"} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`,
|
||||||
|
}))}
|
||||||
|
onItemClick={(id) => {
|
||||||
|
const toolType = toolTypes.find((t) => t.id === id);
|
||||||
|
if (toolType) {
|
||||||
|
onSelect(toolType);
|
||||||
|
onViewChange("detail");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
emptyMessage="No tool types yet"
|
||||||
|
/>
|
||||||
|
<MobileFAB
|
||||||
|
onClick={() => {
|
||||||
|
onCreate();
|
||||||
|
onViewChange("edit");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,370 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { extractErrorMessage } from "../utils/errors";
|
||||||
|
import {
|
||||||
|
createToolType,
|
||||||
|
deleteToolType,
|
||||||
|
listToolTypes,
|
||||||
|
updateToolType,
|
||||||
|
type CreateToolTypeRequest,
|
||||||
|
type ReadinessProbe,
|
||||||
|
type ToolType,
|
||||||
|
type UpdateToolTypeRequest,
|
||||||
|
} from "../api/tool-types";
|
||||||
|
import {
|
||||||
|
createToolDefinition,
|
||||||
|
getToolDefinition,
|
||||||
|
listToolDefinitions,
|
||||||
|
updateToolDefinition,
|
||||||
|
} from "../api/tool-definitions";
|
||||||
|
import type { ToolTypeFormState } from "../components/features/tool-workshop/ToolTypeEditorPanel";
|
||||||
|
|
||||||
|
type Status = "loading" | "ready" | "error";
|
||||||
|
|
||||||
|
const defaultForm: ToolTypeFormState = {
|
||||||
|
name: "",
|
||||||
|
display_name: "",
|
||||||
|
description: "",
|
||||||
|
category: "",
|
||||||
|
interface_type: "web",
|
||||||
|
requires_port: true,
|
||||||
|
default_port: "",
|
||||||
|
definition_type: "compose",
|
||||||
|
compose_template: "",
|
||||||
|
dockerfile_template: "",
|
||||||
|
readiness_command: "",
|
||||||
|
readiness_timeout: "30",
|
||||||
|
readiness_interval: "2",
|
||||||
|
required_variables: "",
|
||||||
|
startup_command: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useToolWorkshop = () => {
|
||||||
|
const [status, setStatus] = useState<Status>("loading");
|
||||||
|
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
|
||||||
|
const [baseDefinitions, setBaseDefinitions] = useState<
|
||||||
|
Awaited<ReturnType<typeof listToolDefinitions>>
|
||||||
|
>([]);
|
||||||
|
const [manifestData, setManifestData] = useState<Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
> | null>(null);
|
||||||
|
const [manifestDefinitionId, setManifestDefinitionId] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
const [selectedToolTypeId, setSelectedToolTypeId] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
|
const [toolTypeForm, setToolTypeForm] = useState<ToolTypeFormState>(defaultForm);
|
||||||
|
const [toolTypeError, setToolTypeError] = useState<string | null>(null);
|
||||||
|
const [toolTypeDirty, setToolTypeDirty] = useState(false);
|
||||||
|
|
||||||
|
const selectedToolType =
|
||||||
|
(toolTypes || []).find((t) => t.id === selectedToolTypeId) || null;
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
setStatus("loading");
|
||||||
|
try {
|
||||||
|
const [types, defs] = await Promise.all([
|
||||||
|
listToolTypes(),
|
||||||
|
listToolDefinitions(),
|
||||||
|
]);
|
||||||
|
setToolTypes(types || []);
|
||||||
|
setBaseDefinitions((defs || []).filter((d) => d.is_base));
|
||||||
|
setStatus("ready");
|
||||||
|
} catch {
|
||||||
|
setStatus("error");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadData();
|
||||||
|
}, [loadData]);
|
||||||
|
|
||||||
|
const resetToolTypeForm = () => {
|
||||||
|
setToolTypeForm(defaultForm);
|
||||||
|
setToolTypeError(null);
|
||||||
|
setToolTypeDirty(false);
|
||||||
|
setManifestData(null);
|
||||||
|
setManifestDefinitionId(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const populateToolTypeForm = async (toolType: ToolType) => {
|
||||||
|
setToolTypeForm({
|
||||||
|
name: toolType.name,
|
||||||
|
display_name: toolType.display_name,
|
||||||
|
description: toolType.description || "",
|
||||||
|
category: toolType.category || "",
|
||||||
|
interface_type: (toolType.interface_type as "web" | "terminal") || "web",
|
||||||
|
requires_port: toolType.requires_port ?? true,
|
||||||
|
default_port: toolType.default_port?.toString() || "",
|
||||||
|
definition_type:
|
||||||
|
(toolType.definition_type as "compose" | "dockerfile" | "manifest") ||
|
||||||
|
"compose",
|
||||||
|
compose_template: toolType.compose_template || "",
|
||||||
|
dockerfile_template: toolType.dockerfile_template || "",
|
||||||
|
readiness_command: toolType.readiness_probe?.command || "",
|
||||||
|
readiness_timeout: toolType.readiness_probe?.timeout?.toString() || "30",
|
||||||
|
readiness_interval: toolType.readiness_probe?.interval?.toString() || "2",
|
||||||
|
required_variables: toolType.required_variables?.join(", ") || "",
|
||||||
|
startup_command: toolType.startup_command || "",
|
||||||
|
});
|
||||||
|
setToolTypeError(null);
|
||||||
|
setToolTypeDirty(false);
|
||||||
|
setManifestDefinitionId(toolType.manifest_id || null);
|
||||||
|
|
||||||
|
if (toolType.definition_type === "manifest" && toolType.manifest_id) {
|
||||||
|
try {
|
||||||
|
const defn = await getToolDefinition(toolType.manifest_id);
|
||||||
|
setManifestData(defn.manifest);
|
||||||
|
} catch {
|
||||||
|
setManifestData(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setManifestData(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectToolType = (toolType: ToolType | null) => {
|
||||||
|
if (toolTypeDirty) {
|
||||||
|
if (!window.confirm("You have unsaved changes. Discard them?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toolType) {
|
||||||
|
setSelectedToolTypeId(toolType.id);
|
||||||
|
setIsCreating(false);
|
||||||
|
void populateToolTypeForm(toolType);
|
||||||
|
} else {
|
||||||
|
setSelectedToolTypeId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateNew = () => {
|
||||||
|
if (toolTypeDirty) {
|
||||||
|
if (!window.confirm("You have unsaved changes. Discard them?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setSelectedToolTypeId(null);
|
||||||
|
setIsCreating(true);
|
||||||
|
resetToolTypeForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToolTypeSubmit = async (e?: React.FormEvent) => {
|
||||||
|
e?.preventDefault();
|
||||||
|
setToolTypeError(null);
|
||||||
|
|
||||||
|
if (!toolTypeForm.name.trim() || !toolTypeForm.display_name.trim()) {
|
||||||
|
setToolTypeError("Name and display name are required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
toolTypeForm.requires_port &&
|
||||||
|
(!toolTypeForm.default_port.trim() ||
|
||||||
|
isNaN(Number(toolTypeForm.default_port)))
|
||||||
|
) {
|
||||||
|
setToolTypeError("Default port is required and must be a number");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolTypeForm.definition_type !== "manifest") {
|
||||||
|
const template =
|
||||||
|
toolTypeForm.definition_type === "compose"
|
||||||
|
? toolTypeForm.compose_template
|
||||||
|
: toolTypeForm.dockerfile_template;
|
||||||
|
|
||||||
|
if (!template.trim()) {
|
||||||
|
setToolTypeError(
|
||||||
|
`${toolTypeForm.definition_type === "compose" ? "Compose" : "Dockerfile"} template is required`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (!manifestData) {
|
||||||
|
setToolTypeError(
|
||||||
|
"Manifest data is required for manifest definition type",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variables = toolTypeForm.required_variables
|
||||||
|
.split(",")
|
||||||
|
.map((v) => v.trim())
|
||||||
|
.filter((v) => v.length > 0);
|
||||||
|
|
||||||
|
const readinessProbe: ReadinessProbe | undefined =
|
||||||
|
toolTypeForm.readiness_command.trim()
|
||||||
|
? {
|
||||||
|
command: toolTypeForm.readiness_command.trim(),
|
||||||
|
timeout: parseInt(toolTypeForm.readiness_timeout) || 30,
|
||||||
|
interval: parseInt(toolTypeForm.readiness_interval) || 2,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const template =
|
||||||
|
toolTypeForm.definition_type === "compose"
|
||||||
|
? toolTypeForm.compose_template
|
||||||
|
: toolTypeForm.dockerfile_template;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isCreating) {
|
||||||
|
let manifestId: string | undefined;
|
||||||
|
if (toolTypeForm.definition_type === "manifest" && manifestData) {
|
||||||
|
const manifestPayload = {
|
||||||
|
name: toolTypeForm.name.trim(),
|
||||||
|
display_name: toolTypeForm.display_name.trim(),
|
||||||
|
description: toolTypeForm.description.trim() || undefined,
|
||||||
|
category: toolTypeForm.category.trim() || undefined,
|
||||||
|
interface_type: toolTypeForm.interface_type,
|
||||||
|
base_image: (manifestData.base_image as string) || undefined,
|
||||||
|
base_definition_id:
|
||||||
|
(manifestData.base_definition_id as string) || undefined,
|
||||||
|
manifest: manifestData,
|
||||||
|
};
|
||||||
|
const newManifest = await createToolDefinition(manifestPayload);
|
||||||
|
manifestId = newManifest.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const input: CreateToolTypeRequest = {
|
||||||
|
name: toolTypeForm.name.trim(),
|
||||||
|
display_name: toolTypeForm.display_name.trim(),
|
||||||
|
description: toolTypeForm.description.trim() || undefined,
|
||||||
|
category: toolTypeForm.category.trim() || undefined,
|
||||||
|
interface_type: toolTypeForm.interface_type,
|
||||||
|
requires_port: toolTypeForm.requires_port,
|
||||||
|
default_port: toolTypeForm.requires_port
|
||||||
|
? Number(toolTypeForm.default_port)
|
||||||
|
: 0,
|
||||||
|
definition_type: toolTypeForm.definition_type,
|
||||||
|
manifest_id: manifestId,
|
||||||
|
compose_template:
|
||||||
|
toolTypeForm.definition_type === "compose" ? template : undefined,
|
||||||
|
dockerfile_template:
|
||||||
|
toolTypeForm.definition_type === "dockerfile"
|
||||||
|
? template
|
||||||
|
: undefined,
|
||||||
|
readiness_probe: readinessProbe,
|
||||||
|
required_variables: variables,
|
||||||
|
startup_command: toolTypeForm.startup_command.trim() || undefined,
|
||||||
|
};
|
||||||
|
const newTool = await createToolType(input);
|
||||||
|
setIsCreating(false);
|
||||||
|
setSelectedToolTypeId(newTool.id);
|
||||||
|
setToolTypeDirty(false);
|
||||||
|
} else if (selectedToolType) {
|
||||||
|
let manifestId = selectedToolType.manifest_id || undefined;
|
||||||
|
if (toolTypeForm.definition_type === "manifest" && manifestData) {
|
||||||
|
if (manifestId) {
|
||||||
|
await updateToolDefinition(manifestId, {
|
||||||
|
display_name: toolTypeForm.display_name.trim(),
|
||||||
|
description: toolTypeForm.description.trim() || undefined,
|
||||||
|
category: toolTypeForm.category.trim() || undefined,
|
||||||
|
manifest: manifestData,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const manifestPayload = {
|
||||||
|
name: toolTypeForm.name.trim(),
|
||||||
|
display_name: toolTypeForm.display_name.trim(),
|
||||||
|
description: toolTypeForm.description.trim() || undefined,
|
||||||
|
category: toolTypeForm.category.trim() || undefined,
|
||||||
|
interface_type: toolTypeForm.interface_type,
|
||||||
|
base_image: (manifestData.base_image as string) || undefined,
|
||||||
|
base_definition_id:
|
||||||
|
(manifestData.base_definition_id as string) || undefined,
|
||||||
|
manifest: manifestData,
|
||||||
|
};
|
||||||
|
const newManifest = await createToolDefinition(manifestPayload);
|
||||||
|
manifestId = newManifest.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const input: UpdateToolTypeRequest = {
|
||||||
|
display_name: toolTypeForm.display_name.trim(),
|
||||||
|
description: toolTypeForm.description.trim() || undefined,
|
||||||
|
category: toolTypeForm.category.trim() || undefined,
|
||||||
|
interface_type: toolTypeForm.interface_type,
|
||||||
|
requires_port: toolTypeForm.requires_port,
|
||||||
|
default_port: toolTypeForm.requires_port
|
||||||
|
? Number(toolTypeForm.default_port)
|
||||||
|
: 0,
|
||||||
|
definition_type: toolTypeForm.definition_type,
|
||||||
|
manifest_id:
|
||||||
|
toolTypeForm.definition_type === "manifest"
|
||||||
|
? manifestId
|
||||||
|
: undefined,
|
||||||
|
compose_template:
|
||||||
|
toolTypeForm.definition_type === "compose" ? template : undefined,
|
||||||
|
dockerfile_template:
|
||||||
|
toolTypeForm.definition_type === "dockerfile"
|
||||||
|
? template
|
||||||
|
: undefined,
|
||||||
|
readiness_probe: readinessProbe,
|
||||||
|
required_variables: variables,
|
||||||
|
startup_command: toolTypeForm.startup_command.trim() || undefined,
|
||||||
|
};
|
||||||
|
await updateToolType(selectedToolType.id, input);
|
||||||
|
setToolTypeDirty(false);
|
||||||
|
}
|
||||||
|
await loadData();
|
||||||
|
} catch (err) {
|
||||||
|
setToolTypeError(extractErrorMessage(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteToolType = async (id: string) => {
|
||||||
|
if (
|
||||||
|
!window.confirm(
|
||||||
|
"Delete this tool type? All associated configs will be removed.",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
await deleteToolType(id);
|
||||||
|
if (selectedToolTypeId === id) {
|
||||||
|
setSelectedToolTypeId(null);
|
||||||
|
setIsCreating(false);
|
||||||
|
resetToolTypeForm();
|
||||||
|
}
|
||||||
|
await loadData();
|
||||||
|
} catch {
|
||||||
|
alert("Failed to delete tool type");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormChange = (changes: Partial<ToolTypeFormState>) => {
|
||||||
|
setToolTypeForm((prev) => ({ ...prev, ...changes }));
|
||||||
|
setToolTypeDirty(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
if (isCreating) {
|
||||||
|
resetToolTypeForm();
|
||||||
|
} else if (selectedToolType) {
|
||||||
|
void populateToolTypeForm(selectedToolType);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
toolTypes,
|
||||||
|
baseDefinitions,
|
||||||
|
selectedToolType,
|
||||||
|
selectedToolTypeId,
|
||||||
|
isCreating,
|
||||||
|
toolTypeForm,
|
||||||
|
manifestData,
|
||||||
|
manifestDefinitionId,
|
||||||
|
toolTypeError,
|
||||||
|
toolTypeDirty,
|
||||||
|
loadData,
|
||||||
|
handleSelectToolType,
|
||||||
|
handleCreateNew,
|
||||||
|
handleToolTypeSubmit,
|
||||||
|
handleDeleteToolType,
|
||||||
|
handleFormChange,
|
||||||
|
handleReset,
|
||||||
|
setManifestData,
|
||||||
|
setToolTypeDirty,
|
||||||
|
};
|
||||||
|
};
|
||||||
+19
-10
@@ -5,16 +5,25 @@ import { BrowserRouter } from "react-router-dom";
|
|||||||
import { AppRouter } from "./router";
|
import { AppRouter } from "./router";
|
||||||
import { AuthProvider } from "./state/auth";
|
import { AuthProvider } from "./state/auth";
|
||||||
import { SessionsProvider } from "./state/sessions";
|
import { SessionsProvider } from "./state/sessions";
|
||||||
import "./styles.css";
|
import "./styles/tokens.css";
|
||||||
|
import "./styles/global.css";
|
||||||
|
import "./styles/utilities.css";
|
||||||
|
import "./styles/syntax-highlight.css";
|
||||||
|
import "./styles/pages/git-history.css";
|
||||||
|
import "./styles/pages/repo-workspace.css";
|
||||||
|
import "./styles/pages/projects.css";
|
||||||
|
import "./styles/pages/sessions.css";
|
||||||
|
import "./styles/pages/ssh-keys.css";
|
||||||
|
import "./styles/pages/workspace-detail.css";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<SessionsProvider>
|
<SessionsProvider>
|
||||||
<AppRouter />
|
<AppRouter />
|
||||||
</SessionsProvider>
|
</SessionsProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</React.StrictMode>
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,257 @@
|
|||||||
|
/* Git History Page Styles */
|
||||||
|
.history-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-selector {
|
||||||
|
padding: 0.45rem 0.7rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
font: inherit;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
min-height: 60vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 70vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-list.with-detail {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-item:hover {
|
||||||
|
background: #ece7df;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-item.selected {
|
||||||
|
border-color: var(--brand);
|
||||||
|
background: #f0f7f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-graph {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--brand);
|
||||||
|
white-space: pre;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-line {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-header {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-hash {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--brand);
|
||||||
|
background: #f0f7f4;
|
||||||
|
padding: 0.15rem 0.4rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-refs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.35rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ref-tag {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.15rem 0.4rem;
|
||||||
|
background: var(--brand);
|
||||||
|
color: white;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-message {
|
||||||
|
margin: 0 0 0.35rem;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-detail-panel {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 70vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section h4 {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-hash-full {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--brand);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-message-full {
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: #f5f3ee;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat.additions {
|
||||||
|
background: #f0fdf4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat.deletions {
|
||||||
|
background: #fef2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat.additions .stat-value {
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat.deletions .stat-value {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.parent-list {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parent-hash {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
background: #f5f3ee;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff-content {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
background: #f5f3ee;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.history-container {
|
||||||
|
grid-template-columns: 1fr 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-list.with-detail {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-detail-panel {
|
||||||
|
grid-column: 2;
|
||||||
|
position: sticky;
|
||||||
|
top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
/* ─── Projects Page Refresh ─── */
|
||||||
|
|
||||||
|
.project-info-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: var(--space-2);
|
||||||
|
border-radius: 10px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-toggle:hover {
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-toggle h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-count {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
background: var(--bg);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-detail {
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
padding-top: var(--space-4);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-4);
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-header h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-chip {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-1);
|
||||||
|
padding: var(--space-3);
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-chip a {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-chip .ws-branch {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-chip .ws-instances {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-chip .ws-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-1);
|
||||||
|
margin-top: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-chip .ws-actions button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: var(--space-1);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-chip .ws-actions button:hover {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-chip .ws-actions button.danger-text:hover {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,709 @@
|
|||||||
|
/* Repository Workspace */
|
||||||
|
.repo-workspace {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-header-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-header-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-header-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-header-subtitle {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-header-action-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-header-action-btn:hover {
|
||||||
|
background: var(--bg);
|
||||||
|
border-color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-title h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-name {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-layout {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-sidebar {
|
||||||
|
width: 280px;
|
||||||
|
min-width: 280px;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
background: var(--panel);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.workspace-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-sidebar {
|
||||||
|
width: 100%;
|
||||||
|
min-width: auto;
|
||||||
|
max-height: 40vh;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-header-actions {
|
||||||
|
width: 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-section {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-section label {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-main {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--bg);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File Tree */
|
||||||
|
.file-tree {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-entry {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.375rem 0.5rem;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: var(--ink);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-entry:hover {
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-directory {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-up {
|
||||||
|
color: var(--muted);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File Viewer */
|
||||||
|
.file-viewer {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-viewer-header {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-breadcrumbs {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-sep {
|
||||||
|
color: var(--muted);
|
||||||
|
margin: 0 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-content {
|
||||||
|
padding: 1rem;
|
||||||
|
overflow: auto;
|
||||||
|
max-height: calc(100vh - 200px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-content pre {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-viewer-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File Status Indicators */
|
||||||
|
.file-status-indicator {
|
||||||
|
float: right;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0 0.375rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-status-indicator.modified {
|
||||||
|
color: #f59e0b;
|
||||||
|
background: rgba(245, 158, 11, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-status-indicator.added {
|
||||||
|
color: #10b981;
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-status-indicator.deleted {
|
||||||
|
color: #ef4444;
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-status-indicator.untracked {
|
||||||
|
color: #6b7280;
|
||||||
|
background: rgba(107, 114, 128, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Commit Panel */
|
||||||
|
.commit-panel {
|
||||||
|
padding: 1rem;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-panel h4 {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list {
|
||||||
|
max-height: 150px;
|
||||||
|
overflow: auto;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-status {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
width: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item.modified .file-status {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
.file-item.added .file-status {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
.file-item.deleted .file-status {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
.file-item.untracked .file-status {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-message-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-error {
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Merge Dialog */
|
||||||
|
.merge-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merge-form .form-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merge-form label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merge-form select,
|
||||||
|
.merge-form input,
|
||||||
|
.merge-form textarea {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merge-form textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-text {
|
||||||
|
color: #10b981;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Git Toolbar - Top Bar Styles */
|
||||||
|
.git-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
background: var(--bg);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.4rem 0.75rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-button:hover:not(:disabled) {
|
||||||
|
background: var(--brand);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-button.primary {
|
||||||
|
background: var(--brand);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-select {
|
||||||
|
padding: 0.4rem 0.75rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0 4px;
|
||||||
|
background: var(--brand);
|
||||||
|
color: white;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-error {
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-input {
|
||||||
|
padding: 0.4rem 0.75rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-branch-form {
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-summary {
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.modified {
|
||||||
|
background: rgba(245, 158, 11, 0.1);
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.added {
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.deleted {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.untracked {
|
||||||
|
background: rgba(107, 114, 128, 0.1);
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File Editor */
|
||||||
|
.file-editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-editor-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: var(--panel);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-editor-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Git Mount Editor Styles */
|
||||||
|
.git-mount-editor {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-mount-editor .section-subtitle {
|
||||||
|
margin: 0 0 0.75rem 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-mount-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-mount-item {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-mount-display {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-mount-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-mount-repo {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-mount-paths {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-mount-branch {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--accent);
|
||||||
|
background: var(--accent-bg);
|
||||||
|
padding: 0.125rem 0.375rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-mount-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-mount-add {
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
padding-top: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-mount-add h5 {
|
||||||
|
margin: 0 0 0.75rem 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-mount-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-mount-form .form-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-mount-form .form-row label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-mount-form .form-row input,
|
||||||
|
.git-mount-form .form-row select {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-mount-form .form-row input.error,
|
||||||
|
.git-mount-form .form-row select.error {
|
||||||
|
border-color: #cd3131;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-mount-form .form-row .hint {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-mount-form .form-row .error-text {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #cd3131;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-mount-form .form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-repo-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-repo-form input {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-repo-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/* Mobile Sessions Page */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.sessions-page {
|
||||||
|
padding: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sessions-page .page-header {
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sessions-page .page-header h1 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-session-section {
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-session-section h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-session-section {
|
||||||
|
padding: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-session-section h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-card-actions.mobile {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding-top: var(--space-3);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-card-actions.mobile .mobile-primary {
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-card-actions.mobile .mobile-more {
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--space-2);
|
||||||
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.create-session-form .form-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-session-form input,
|
||||||
|
.create-session-form select,
|
||||||
|
.create-session-form textarea,
|
||||||
|
.create-session-form button {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/* SSH keys table responsive */
|
||||||
|
.ssh-key-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ssh-key-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-4);
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.ssh-key-item {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,504 @@
|
|||||||
|
/* ─── Workspace Detail Page ─── */
|
||||||
|
|
||||||
|
.workspace-detail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-header-link {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--space-4) var(--space-5);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--panel);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-breadcrumb .sep {
|
||||||
|
color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-breadcrumb strong {
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
padding: var(--space-1) var(--space-3);
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab Bar */
|
||||||
|
.tab-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-1);
|
||||||
|
padding: var(--space-2) var(--space-5);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--panel);
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
padding: var(--space-2) var(--space-4);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: none;
|
||||||
|
color: var(--muted);
|
||||||
|
font: inherit;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: var(--brand);
|
||||||
|
color: var(--primary-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Tab Bar */
|
||||||
|
.mobile-tab-bar {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: var(--space-2) 0;
|
||||||
|
background: var(--panel);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-tab {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: var(--muted);
|
||||||
|
font: inherit;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-tab.active {
|
||||||
|
color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Workspace Content */
|
||||||
|
.workspace-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: var(--space-4) var(--space-5);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Files Tab */
|
||||||
|
.files-tab {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-3);
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-toolbar-status {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-toolbar-status span {
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-modified {
|
||||||
|
background: var(--warning-light);
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-added {
|
||||||
|
background: var(--success-light);
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-deleted {
|
||||||
|
background: var(--danger-light);
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-untracked {
|
||||||
|
background: rgba(107, 114, 128, 0.1);
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-toolbar-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-toolbar-actions input {
|
||||||
|
padding: var(--space-1) var(--space-3);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
font: inherit;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--ink);
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-split {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 260px 1fr;
|
||||||
|
gap: var(--space-4);
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree {
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: var(--space-3);
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-entry {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: none;
|
||||||
|
color: var(--ink);
|
||||||
|
font: inherit;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-entry:hover {
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-entry.selected {
|
||||||
|
background: var(--brand);
|
||||||
|
color: var(--primary-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-viewer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--panel);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-viewer-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--space-3);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--space-4);
|
||||||
|
overflow: auto;
|
||||||
|
margin: 0;
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-editor {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--space-3);
|
||||||
|
border: none;
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
line-height: 1.6;
|
||||||
|
resize: none;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-editor-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-3);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Git Tab */
|
||||||
|
.git-tab {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-tab-header {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-3);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-tab-header select {
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
font: inherit;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-history {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 60px 1fr 120px 120px;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-3);
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-hash {
|
||||||
|
font-family: monospace;
|
||||||
|
color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-message {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-author,
|
||||||
|
.commit-date {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tools Tab */
|
||||||
|
.tools-tab {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-10);
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state-card h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state-card p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.instances-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-4);
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-card.running {
|
||||||
|
border-color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings Tab */
|
||||||
|
.settings-tab {
|
||||||
|
max-width: 640px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding: var(--space-5);
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Workspace Detail */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.workspace-detail.mobile .workspace-content {
|
||||||
|
padding-bottom: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-tab-bar {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-split {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-toolbar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Workspace Card Link */
|
||||||
|
.workspace-header-link {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
margin: -1rem -1rem 0;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-header-link:hover .workspace-header h4 {
|
||||||
|
color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Workspace Create Inline ─── */
|
||||||
|
|
||||||
|
.workspace-create-inline {
|
||||||
|
padding: var(--space-5);
|
||||||
|
margin-bottom: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-create-inline h3 {
|
||||||
|
margin: 0 0 var(--space-4) 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-create-form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: var(--space-4);
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-create-form-grid .form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-create-form-grid label {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-create-form-grid input,
|
||||||
|
.workspace-create-form-grid select {
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
font: inherit;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-create-form-grid .form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-3);
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
/* Syntax Highlighter */
|
||||||
|
.syntax-highlighter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlighter-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: var(--panel);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-badge {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
background: var(--bg);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button:hover {
|
||||||
|
background: var(--brand);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-container {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
font-family: "Fira Code", "Monaco", "Courier New", monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-numbers {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1rem 0.5rem;
|
||||||
|
background: var(--panel);
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
color: var(--muted);
|
||||||
|
text-align: right;
|
||||||
|
user-select: none;
|
||||||
|
min-width: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-number {
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow: visible;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block code {
|
||||||
|
display: block;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code Editor */
|
||||||
|
.code-editor {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-textarea {
|
||||||
|
font-family: "Fira Code", "Monaco", "Courier New", monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-textarea-input {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ink);
|
||||||
|
caret-color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-line {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-line-number {
|
||||||
|
display: inline-block;
|
||||||
|
width: 3rem;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
text-align: right;
|
||||||
|
color: var(--muted);
|
||||||
|
user-select: none;
|
||||||
|
background: var(--panel);
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-line-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prism.js Theme Integration */
|
||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
color: var(--ink);
|
||||||
|
text-shadow: none;
|
||||||
|
font-family: "Fira Code", "Monaco", "Courier New", monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre;
|
||||||
|
word-spacing: normal;
|
||||||
|
word-break: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
line-height: 1.5;
|
||||||
|
tab-size: 2;
|
||||||
|
hyphens: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Syntax Highlighting Colors */
|
||||||
|
.token.comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.doctype,
|
||||||
|
.token.cdata {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.punctuation {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.namespace {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.property,
|
||||||
|
.token.tag,
|
||||||
|
.token.boolean,
|
||||||
|
.token.number,
|
||||||
|
.token.constant,
|
||||||
|
.token.symbol,
|
||||||
|
.token.deleted {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.selector,
|
||||||
|
.token.attr-name,
|
||||||
|
.token.string,
|
||||||
|
.token.char,
|
||||||
|
.token.builtin,
|
||||||
|
.token.inserted {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.operator,
|
||||||
|
.token.entity,
|
||||||
|
.token.url,
|
||||||
|
.language-css .token.string,
|
||||||
|
.style .token.string {
|
||||||
|
color: #f43f5e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.atrule,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.keyword {
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.function,
|
||||||
|
.token.class-name {
|
||||||
|
color: #8b5cf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.regex,
|
||||||
|
.token.important,
|
||||||
|
.token.variable {
|
||||||
|
color: #ec4899;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
font-family: "Inter", "IBM Plex Sans", "Segoe UI", sans-serif;
|
||||||
|
--bg: #f4f1ea;
|
||||||
|
--panel: #fffef9;
|
||||||
|
--ink: #1d1d1b;
|
||||||
|
--muted: #5f5b55;
|
||||||
|
--brand: #275d4b;
|
||||||
|
--brand-strong: #154236;
|
||||||
|
--border: #d8d0c5;
|
||||||
|
--primary: #275d4b;
|
||||||
|
--primary-fg: #fffef9;
|
||||||
|
--color-primary: #275d4b;
|
||||||
|
--success: #2f8f62;
|
||||||
|
--success-light: rgba(47, 143, 98, 0.14);
|
||||||
|
--warning: #c08a1e;
|
||||||
|
--warning-light: rgba(192, 138, 30, 0.14);
|
||||||
|
--danger: #b94a3c;
|
||||||
|
--danger-light: rgba(185, 74, 60, 0.14);
|
||||||
|
--info: #4f7fb8;
|
||||||
|
--info-light: rgba(79, 127, 184, 0.14);
|
||||||
|
|
||||||
|
/* Spacing Scale (4px base) */
|
||||||
|
--space-1: 0.25rem;
|
||||||
|
--space-2: 0.5rem;
|
||||||
|
--space-3: 0.75rem;
|
||||||
|
--space-4: 1rem;
|
||||||
|
--space-5: 1.5rem;
|
||||||
|
--space-6: 2rem;
|
||||||
|
--space-8: 3rem;
|
||||||
|
--space-10: 4rem;
|
||||||
|
|
||||||
|
/* Breakpoints */
|
||||||
|
--bp-sm: 480px;
|
||||||
|
--bp-md: 768px;
|
||||||
|
--bp-lg: 1024px;
|
||||||
|
--bp-xl: 1280px;
|
||||||
|
|
||||||
|
/* Fluid Typography */
|
||||||
|
--font-size-xs: clamp(0.625rem, 0.6rem + 0.125vw, 0.75rem);
|
||||||
|
--font-size-sm: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
|
||||||
|
--font-size-base: clamp(0.875rem, 0.8rem + 0.35vw, 1rem);
|
||||||
|
--font-size-lg: clamp(1rem, 0.9rem + 0.5vw, 1.25rem);
|
||||||
|
--font-size-xl: clamp(1.25rem, 1.1rem + 0.75vw, 1.5rem);
|
||||||
|
--font-size-2xl: clamp(1.5rem, 1.3rem + 1vw, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bg: #171613;
|
||||||
|
--panel: #22201d;
|
||||||
|
--ink: #ece7df;
|
||||||
|
--muted: #a59d92;
|
||||||
|
--brand: #5fa889;
|
||||||
|
--brand-strong: #4d9175;
|
||||||
|
--border: #39342d;
|
||||||
|
--primary: #5fa889;
|
||||||
|
--primary-fg: #171613;
|
||||||
|
--color-primary: #5fa889;
|
||||||
|
--success: #22c55e;
|
||||||
|
--success-light: rgba(34, 197, 94, 0.15);
|
||||||
|
--warning: #f59e0b;
|
||||||
|
--warning-light: rgba(245, 158, 11, 0.15);
|
||||||
|
--danger: #ef4444;
|
||||||
|
--danger-light: rgba(239, 68, 68, 0.15);
|
||||||
|
--info: #3b82f6;
|
||||||
|
--info-light: rgba(59, 130, 246, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.terminal-page-open,
|
||||||
|
body.terminal-page-open {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] body {
|
||||||
|
background: radial-gradient(circle at top right, #2a2520, var(--bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user