refactor: extract global styles and tokens (Task 2.1)

- Create styles/tokens.css with CSS custom properties and dark theme
- Create styles/global.css with resets, shell layout, and navigation
- Create styles/utilities.css with generic utilities and primitives
- Create styles/syntax-highlight.css with Prism.js theme
- Update main.tsx to import the 4 new style files
- Keep styles.css intact for backward compatibility

Quality gates: build (pass), lint (pass)
Refs: repo-restructure Task 2.1
This commit is contained in:
Developer
2026-06-02 19:29:38 +00:00
parent c50d6663d5
commit c5fbb6722b
17 changed files with 1750 additions and 47 deletions
+1
View File
@@ -0,0 +1 @@
"""Pydantic request/response schemas."""
+42
View File
@@ -0,0 +1,42 @@
"""Config folder request/response schemas."""
from pydantic import BaseModel, Field
class ConfigFolderCreate(BaseModel):
name: str = Field(description="Folder name")
description: str | None = Field(default=None, description="Optional description")
mount_path: str = Field(description="Mount path in container")
files: dict[str, str] | None = Field(
default=None, description="Files as {path: content}"
)
is_active: bool = Field(default=True, description="Whether folder is active")
class ConfigFolderUpdate(BaseModel):
name: str | None = None
description: str | None = None
mount_path: str | None = None
files: dict[str, str] | None = None
is_active: bool | None = None
class ProjectOverrideCreate(BaseModel):
project_id: str = Field(description="Project ID to override for")
mount_path: str | None = Field(default=None, description="Override mount path")
files: dict[str, str] | None = Field(
default=None, description="Override files"
)
is_active: bool | None = Field(default=None, description="Override active state")
class ConfigFolderResponse(BaseModel):
id: str
user_id: str
name: str
description: str | None
mount_path: str
files: dict[str, str] | None
is_active: bool
created_at: str
updated_at: str
+131
View File
@@ -0,0 +1,131 @@
"""Config profile request/response schemas."""
from typing import Any
from pydantic import BaseModel, Field, field_validator
MAX_MOUNT_PATH_LENGTH = 1024
class ConfigProfileCreate(BaseModel):
name: str = Field(description="Profile name (unique per user)")
description: str | None = Field(default=None, description="Optional description")
@field_validator("name")
@classmethod
def validate_name(cls, v: str) -> str:
v = v.strip()
if not v:
raise ValueError("Profile name cannot be empty")
if len(v) > 255:
raise ValueError("Profile name must be 255 characters or less")
return v
class ConfigProfileUpdate(BaseModel):
name: str | None = Field(default=None, description="Profile name")
description: str | None = Field(default=None, description="Optional description")
@field_validator("name")
@classmethod
def validate_name(cls, v: str | None) -> str | None:
if v is None:
return v
v = v.strip()
if not v:
raise ValueError("Profile name cannot be empty")
if len(v) > 255:
raise ValueError("Profile name must be 255 characters or less")
return v
class ConfigProfileResponse(BaseModel):
id: str
user_id: str
name: str
description: str | None
created_at: str
updated_at: str
class ConfigProfileDetailResponse(ConfigProfileResponse):
includes: list[dict[str, Any]]
mounts: list[dict[str, Any]]
class ConfigIncludeCreate(BaseModel):
included_profile_id: str = Field(description="UUID of the profile to include")
order_index: int = Field(default=0, description="Order index for include resolution")
class ConfigIncludeUpdate(BaseModel):
order_index: int = Field(description="Order index for include resolution")
class ConfigIncludeResponse(BaseModel):
id: str
profile_id: str
included_profile_id: str
included_profile_name: str | None
order_index: int
created_at: str
updated_at: str
class ConfigMountCreate(BaseModel):
target_path: str = Field(description="Absolute target path in container")
mode: str = Field(default="rw", description="Mount mode (rw or ro)")
files: dict[str, str] | None = Field(
default=None, description="Files as {path: content}"
)
order_index: int = Field(default=0, description="Order index for mount resolution")
@field_validator("target_path")
@classmethod
def validate_target_path(cls, v: str) -> str:
if not v.startswith("/"):
raise ValueError("Target path must be absolute (start with /)")
if ".." in v:
raise ValueError("Target path cannot contain parent directory references (..)")
if len(v) > MAX_MOUNT_PATH_LENGTH:
raise ValueError(f"Target path must be {MAX_MOUNT_PATH_LENGTH} characters or less")
return v
class ConfigMountUpdate(BaseModel):
target_path: str | None = Field(default=None, description="Absolute target path in container")
mode: str | None = Field(default=None, description="Mount mode (rw or ro)")
files: dict[str, str] | None = Field(
default=None, description="Files as {path: content}"
)
order_index: int | None = Field(default=None, description="Order index for mount resolution")
@field_validator("target_path")
@classmethod
def validate_target_path(cls, v: str | None) -> str | None:
if v is None:
return v
if not v.startswith("/"):
raise ValueError("Target path must be absolute (start with /)")
if ".." in v:
raise ValueError("Target path cannot contain parent directory references (..)")
if len(v) > MAX_MOUNT_PATH_LENGTH:
raise ValueError(f"Target path must be {MAX_MOUNT_PATH_LENGTH} characters or less")
return v
class ConfigMountResponse(BaseModel):
id: str
profile_id: str
target_path: str
mode: str
files: dict[str, str] | None
order_index: int
created_at: str
updated_at: str
class DefaultProfilesUpdate(BaseModel):
default_profiles: dict[str, str] = Field(
description="Mapping of tool_type_id to profile_id"
)
+129
View File
@@ -0,0 +1,129 @@
"""Git repository request/response schemas."""
import uuid
from datetime import datetime
from pydantic import BaseModel, ConfigDict
class GitRepositoryCreate(BaseModel):
name: str
remote_url: str | None = None
force_original_url: bool = False
class URLParseRequest(BaseModel):
url: str
class URLParseResponse(BaseModel):
original_url: str
base_url: str | None
is_valid_clone_url: bool
needs_parsing: bool
host: str | None
message: str
error_code: str | None
class GitRepositoryResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
name: str
path: str
project_id: uuid.UUID
owner_id: uuid.UUID
is_mirror: bool
remote_url: str | None
last_push: datetime | None
created_at: datetime
updated_at: datetime
class FileListResponse(BaseModel):
path: str
branch: str
entries: list[dict]
class FileContentResponse(BaseModel):
path: str
branch: str
content: str
size: int
encoding: str
language: str | None
is_binary: bool
last_commit: dict | None
class BranchesResponse(BaseModel):
branches: list[dict]
default_branch: str
class FileUpdateRequest(BaseModel):
path: str
branch: str
content: str
commit_message: str
class FileUpdateResponse(BaseModel):
commit_hash: str
message: str
branch: str
class StatusResponse(BaseModel):
branch: str
modified: list[str]
added: list[str]
deleted: list[str]
untracked: list[str]
renamed: list[str]
ahead: int
behind: int
class BranchCreateRequest(BaseModel):
name: str
base_branch: str = "HEAD"
class CheckoutRequest(BaseModel):
branch: str
class CommitRequest(BaseModel):
message: str
files: list[str] | None = None
class CommitResponse(BaseModel):
commit_hash: str
message: str
class FetchResponse(BaseModel):
message: str
class PullResponse(BaseModel):
message: str
class PushResponse(BaseModel):
message: str
class MergeRequest(BaseModel):
source_branch: str
target_branch: str | None = None
message: str | None = None
class MergeResponse(BaseModel):
commit_hash: str
message: str
+25
View File
@@ -0,0 +1,25 @@
"""Project request/response schemas."""
from pydantic import BaseModel
class ProjectCreate(BaseModel):
name: str
description: str | None = None
class ProjectUpdate(BaseModel):
name: str | None = None
description: str | None = None
class ProjectResponse(BaseModel):
id: str
name: str
description: str | None
created_at: str
updated_at: str
class SetDefaultSSHKeyRequest(BaseModel):
ssh_key_id: str
+16
View File
@@ -0,0 +1,16 @@
"""SSH key request/response schemas."""
from pydantic import BaseModel
class SSHKeyCreate(BaseModel):
name: str
public_key: str
class SSHKeyResponse(BaseModel):
id: str
name: str
public_key: str
fingerprint: str
created_at: str
+47
View File
@@ -0,0 +1,47 @@
"""Tool config request/response schemas."""
from pydantic import BaseModel, Field
class ToolConfigCreate(BaseModel):
tool_type_id: str = Field(description="UUID of the tool type")
key: str = Field(description="Configuration key")
value: str = Field(description="Configuration value")
config_type: str = Field(default="env", description="Config type: env or file")
file_path: str | None = Field(default=None, description="File path for file configs")
port_override: int | None = Field(default=None, description="Port override")
start_command: str | None = Field(default=None, description="Start command override")
working_directory: str | None = Field(default=None, description="Working directory")
environment_variables: dict[str, str] | None = Field(
default=None, description="Additional environment variables"
)
volumes: list[dict] | None = Field(default=None, description="Volume mounts")
class ToolConfigUpdate(BaseModel):
value: str | None = None
config_type: str | None = None
file_path: str | None = None
port_override: int | None = None
start_command: str | None = None
working_directory: str | None = None
environment_variables: dict[str, str] | None = None
volumes: list[dict] | None = None
class ToolConfigResponse(BaseModel):
id: str
tool_type_id: str
user_id: str
project_id: str | None
key: str
value: str
config_type: str
file_path: str | None
port_override: int | None
start_command: str | None
working_directory: str | None
environment_variables: dict[str, str] | None
volumes: list[dict] | None
created_at: str
updated_at: str
+17
View File
@@ -0,0 +1,17 @@
"""Tool instance request/response schemas."""
from pydantic import BaseModel, Field
class CreateInstanceRequest(BaseModel):
"""Request body for creating a tool instance."""
model_config = {"extra": "ignore"}
tool_type_id: str = Field(description="UUID of the tool type to instantiate")
display_name: str | None = Field(
default=None, description="Optional display name for the instance"
)
config_profile_id: str | None = Field(
default=None, description="Optional config profile ID to apply to the instance"
)
+204
View File
@@ -0,0 +1,204 @@
"""Tool type request/response schemas."""
import uuid
from datetime import datetime
import yaml
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
class ToolTypeCreate(BaseModel):
name: str
display_name: str
description: str | None = None
default_port: int
definition_type: str = "compose"
compose_template: str | None = None
dockerfile_template: str | None = None
build_context: dict | None = None
readiness_probe: dict | None = None
required_variables: list[str] = []
category: str = "other"
interfaces: list[str] = ["web"]
@field_validator("definition_type")
@classmethod
def validate_definition_type(cls, v: str) -> str:
if v not in ("compose", "dockerfile"):
raise ValueError("definition_type must be 'compose' or 'dockerfile'")
return v
@field_validator("compose_template")
@classmethod
def validate_compose_template(cls, v: str | None, info) -> str | None:
data = info.data
if data.get("definition_type") != "compose":
return v
if v is None:
raise ValueError("compose_template is required when definition_type is 'compose'")
try:
parsed = yaml.safe_load(v)
except yaml.YAMLError as e:
raise ValueError(f"Invalid YAML: {e}")
if not isinstance(parsed, dict):
raise ValueError("Compose template must be a YAML mapping")
if "services" not in parsed:
raise ValueError("Compose template must contain 'services' key")
if not parsed["services"]:
raise ValueError("Compose template must define at least one service")
return v
@field_validator("dockerfile_template")
@classmethod
def validate_dockerfile_template(cls, v: str | None, info) -> str | None:
data = info.data
if data.get("definition_type") != "dockerfile":
return v
if v is None:
raise ValueError("dockerfile_template is required when definition_type is 'dockerfile'")
if not v.strip().startswith("FROM"):
raise ValueError("Dockerfile must start with a FROM instruction")
return v
@field_validator("default_port")
@classmethod
def validate_default_port(cls, v: int, info) -> int:
if v <= 0 or v > 65535:
raise ValueError("Port must be between 1 and 65535")
data = info.data
if data.get("definition_type") != "compose":
return v
template = data.get("compose_template")
if not template:
return v
try:
parsed = yaml.safe_load(template)
except yaml.YAMLError:
return v
port_str = str(v)
port_exposed = False
if isinstance(parsed, dict) and "services" in parsed:
for service_config in parsed["services"].values():
if isinstance(service_config, dict) and "ports" in service_config:
for port_mapping in service_config["ports"]:
if isinstance(port_mapping, str) and port_str in port_mapping:
port_exposed = True
break
elif isinstance(port_mapping, int) and port_mapping == v:
port_exposed = True
break
if port_exposed:
break
if not port_exposed:
raise ValueError(f"Port {v} is not exposed in the compose template. Add it to the 'ports' section.")
return v
@field_validator("required_variables")
@classmethod
def validate_required_variables(cls, v: list[str], info) -> list[str]:
if not v:
return v
data = info.data
if data.get("definition_type") != "compose":
return v
template = data.get("compose_template")
if not template:
return v
for var in v:
placeholder = f"{{{{{var}}}}}"
if placeholder not in template:
raise ValueError(f"Required variable '{var}' not found in compose template")
return v
@model_validator(mode="after")
def validate_templates(self) -> "ToolTypeCreate":
if self.definition_type == "dockerfile" and self.dockerfile_template is None:
raise ValueError("dockerfile_template is required when definition_type is 'dockerfile'")
if self.definition_type == "compose" and self.compose_template is None:
raise ValueError("compose_template is required when definition_type is 'compose'")
return self
class ToolTypeUpdate(BaseModel):
display_name: str | None = None
description: str | None = None
default_port: int | None = None
definition_type: str | None = None
compose_template: str | None = None
dockerfile_template: str | None = None
build_context: dict | None = None
readiness_probe: dict | None = None
required_variables: list[str] | None = None
category: str | None = None
interfaces: list[str] | None = None
@field_validator("definition_type")
@classmethod
def validate_definition_type(cls, v: str | None) -> str | None:
if v is None:
return v
if v not in ("compose", "dockerfile"):
raise ValueError("definition_type must be 'compose' or 'dockerfile'")
return v
@field_validator("compose_template")
@classmethod
def validate_compose_template(cls, v: str | None, info) -> str | None:
if v is None:
return v
data = info.data
definition_type = data.get("definition_type")
if definition_type and definition_type != "compose":
return v
try:
parsed = yaml.safe_load(v)
except yaml.YAMLError as e:
raise ValueError(f"Invalid YAML: {e}")
if not isinstance(parsed, dict):
raise ValueError("Compose template must be a YAML mapping")
if "services" not in parsed:
raise ValueError("Compose template must contain 'services' key")
if not parsed["services"]:
raise ValueError("Compose template must define at least one service")
return v
@field_validator("dockerfile_template")
@classmethod
def validate_dockerfile_template(cls, v: str | None, info) -> str | None:
if v is None:
return v
data = info.data
definition_type = data.get("definition_type")
if definition_type and definition_type != "dockerfile":
return v
if not v.strip().startswith("FROM"):
raise ValueError("Dockerfile must start with a FROM instruction")
return v
class ToolTypeResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
name: str
display_name: str
description: str | None
category: str
interfaces: list[str]
default_port: int
definition_type: str
compose_template: str | None
dockerfile_template: str | None
build_context: dict | None
readiness_probe: dict | None
required_variables: list[str]
is_builtin: bool
created_by_id: uuid.UUID | None
created_at: datetime
updated_at: datetime
class ToolTypeValidateRequest(BaseModel):
definition_type: str
compose_template: str | None = None
dockerfile_template: str | None = None