feat(FN-1846): add mobile responsive styles for node modals
- Add mobile responsive styles for AddNodeModal and ConnectNodeModal - Create comprehensive AddNodeModal tests with type toggle, validation, and transitions - Improve CSS consistency across node modals with proper spacing and transitions - Refactor AddNodeModal TSX markup with clean structure and type toggle - Remove duplicate CSS block in ConnectNodeModal
This commit is contained in:
@@ -109,7 +109,7 @@ export function AddNodeModal({ isOpen, onClose, onSubmit, addToast }: AddNodeMod
|
||||
|
||||
try {
|
||||
await onSubmit(input);
|
||||
addToast(`Node \"${input.name}\" registered`, "success");
|
||||
addToast(`Node "${input.name}" registered`, "success");
|
||||
closeModal();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to register node";
|
||||
@@ -132,6 +132,8 @@ export function AddNodeModal({ isOpen, onClose, onSubmit, addToast }: AddNodeMod
|
||||
</div>
|
||||
|
||||
<div className="modal-body add-node-modal__body">
|
||||
<p className="add-node-modal__description">Register a node to distribute task execution across machines.</p>
|
||||
|
||||
<label className="add-node-modal__field">
|
||||
<span>Name</span>
|
||||
<input
|
||||
@@ -146,45 +148,54 @@ export function AddNodeModal({ isOpen, onClose, onSubmit, addToast }: AddNodeMod
|
||||
{errors.name && <span className="add-node-modal__error">{errors.name}</span>}
|
||||
</label>
|
||||
|
||||
<label className="add-node-modal__field">
|
||||
<span>Type</span>
|
||||
<select
|
||||
value={type}
|
||||
onChange={(event) => setType(event.target.value as "local" | "remote")}
|
||||
<div className="add-node-modal__type-toggle">
|
||||
<button
|
||||
type="button"
|
||||
className={`add-node-modal__type-btn ${type === "local" ? "active" : ""}`}
|
||||
data-type="local"
|
||||
onClick={() => setType("local")}
|
||||
disabled={isSubmitting}
|
||||
aria-pressed={type === "local"}
|
||||
>
|
||||
<option value="local">Local</option>
|
||||
<option value="remote">Remote</option>
|
||||
</select>
|
||||
</label>
|
||||
Local
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`add-node-modal__type-btn ${type === "remote" ? "active" : ""}`}
|
||||
data-type="remote"
|
||||
onClick={() => setType("remote")}
|
||||
disabled={isSubmitting}
|
||||
aria-pressed={type === "remote"}
|
||||
>
|
||||
Remote
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{type === "remote" && (
|
||||
<>
|
||||
<label className="add-node-modal__field">
|
||||
<span>URL</span>
|
||||
<input
|
||||
type="text"
|
||||
value={url}
|
||||
onChange={(event) => setUrl(event.target.value)}
|
||||
placeholder="https://node.example.com"
|
||||
disabled={isSubmitting}
|
||||
aria-invalid={Boolean(errors.url)}
|
||||
/>
|
||||
{errors.url && <span className="add-node-modal__error">{errors.url}</span>}
|
||||
</label>
|
||||
<div className="add-node-modal__remote-fields" data-testid="remote-fields-container" data-visible={type === "remote"}>
|
||||
<label className="add-node-modal__field">
|
||||
<span>URL</span>
|
||||
<input
|
||||
type="text"
|
||||
value={url}
|
||||
onChange={(event) => setUrl(event.target.value)}
|
||||
placeholder="https://node.example.com"
|
||||
disabled={isSubmitting}
|
||||
aria-invalid={Boolean(errors.url)}
|
||||
/>
|
||||
{errors.url && <span className="add-node-modal__error">{errors.url}</span>}
|
||||
</label>
|
||||
|
||||
<label className="add-node-modal__field">
|
||||
<span>API Key</span>
|
||||
<input
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(event) => setApiKey(event.target.value)}
|
||||
placeholder="Optional"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
<label className="add-node-modal__field">
|
||||
<span>API Key</span>
|
||||
<input
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(event) => setApiKey(event.target.value)}
|
||||
placeholder="Optional"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="add-node-modal__field">
|
||||
<span>Max Concurrent</span>
|
||||
@@ -197,13 +208,14 @@ export function AddNodeModal({ isOpen, onClose, onSubmit, addToast }: AddNodeMod
|
||||
disabled={isSubmitting}
|
||||
aria-invalid={Boolean(errors.maxConcurrent)}
|
||||
/>
|
||||
<span className="add-node-modal__hint">Max simultaneous task agents (1–10)</span>
|
||||
{errors.maxConcurrent && <span className="add-node-modal__error">{errors.maxConcurrent}</span>}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="modal-actions">
|
||||
<button className="btn btn-sm" onClick={closeModal} disabled={isSubmitting}>Cancel</button>
|
||||
<button className="btn btn-primary btn-sm" onClick={handleSubmit} disabled={isSubmitting}>
|
||||
<button className="btn btn-primary btn-sm" data-testid="add-node-submit" onClick={handleSubmit} disabled={isSubmitting}>
|
||||
{isSubmitting ? "Adding..." : "Add Node"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { AddNodeModal } from "../AddNodeModal";
|
||||
import type { NodeInfo } from "../../api";
|
||||
|
||||
describe("AddNodeModal", () => {
|
||||
const defaultProps = {
|
||||
isOpen: true,
|
||||
onClose: vi.fn(),
|
||||
onSubmit: vi.fn().mockResolvedValue(undefined),
|
||||
addToast: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders when isOpen is true", () => {
|
||||
render(<AddNodeModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByLabelText("Add Node")).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("Build Machine")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render when isOpen is false", () => {
|
||||
render(<AddNodeModal {...defaultProps} isOpen={false} />);
|
||||
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("validates required name field", async () => {
|
||||
render(<AddNodeModal {...defaultProps} />);
|
||||
|
||||
// Try to submit without filling name
|
||||
fireEvent.click(screen.getByRole("button", { name: "Add Node" }));
|
||||
|
||||
expect(await screen.findByText("Name is required")).toBeInTheDocument();
|
||||
expect(defaultProps.addToast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("validates URL required when type is remote", async () => {
|
||||
render(<AddNodeModal {...defaultProps} />);
|
||||
|
||||
// Switch to remote type
|
||||
fireEvent.click(screen.getByRole("button", { name: "Remote" }));
|
||||
|
||||
// Fill name but not URL
|
||||
fireEvent.change(screen.getByPlaceholderText("Build Machine"), {
|
||||
target: { value: "Remote Node" },
|
||||
});
|
||||
|
||||
// Try to submit
|
||||
fireEvent.click(screen.getByRole("button", { name: "Add Node" }));
|
||||
|
||||
expect(await screen.findByText("URL is required for remote nodes")).toBeInTheDocument();
|
||||
expect(defaultProps.addToast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("validates max concurrent range", async () => {
|
||||
render(<AddNodeModal {...defaultProps} />);
|
||||
|
||||
// Fill name
|
||||
fireEvent.change(screen.getByPlaceholderText("Build Machine"), {
|
||||
target: { value: "Test Node" },
|
||||
});
|
||||
|
||||
// Get the number input (type="number" with min/max)
|
||||
const maxConcurrentInput = screen.getByRole("spinbutton");
|
||||
fireEvent.change(maxConcurrentInput, {
|
||||
target: { value: "15" },
|
||||
});
|
||||
|
||||
// Try to submit
|
||||
fireEvent.click(screen.getByRole("button", { name: "Add Node" }));
|
||||
|
||||
expect(await screen.findByText("Concurrency must be between 1 and 10")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onSubmit with correct input on valid submission", async () => {
|
||||
render(<AddNodeModal {...defaultProps} />);
|
||||
|
||||
// Fill form
|
||||
fireEvent.change(screen.getByPlaceholderText("Build Machine"), {
|
||||
target: { value: "Test Node" },
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Add Node" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(defaultProps.onSubmit).toHaveBeenCalledWith({
|
||||
name: "Test Node",
|
||||
type: "local",
|
||||
url: undefined,
|
||||
apiKey: undefined,
|
||||
maxConcurrent: 2,
|
||||
});
|
||||
expect(defaultProps.addToast).toHaveBeenCalledWith('Node "Test Node" registered', "success");
|
||||
expect(defaultProps.onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows error toast on submission failure", async () => {
|
||||
const errorOnSubmit = vi.fn().mockRejectedValue(new Error("Network error"));
|
||||
render(<AddNodeModal {...defaultProps} onSubmit={errorOnSubmit} />);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText("Build Machine"), {
|
||||
target: { value: "Test Node" },
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Add Node" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(defaultProps.addToast).toHaveBeenCalledWith("Network error", "error");
|
||||
expect(defaultProps.onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("resets form on close", () => {
|
||||
render(<AddNodeModal {...defaultProps} />);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText("Build Machine"), {
|
||||
target: { value: "Test Node" },
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
||||
|
||||
expect(defaultProps.onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles Escape key to close", () => {
|
||||
render(<AddNodeModal {...defaultProps} />);
|
||||
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
|
||||
expect(defaultProps.onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("toggles between local and remote type", async () => {
|
||||
render(<AddNodeModal {...defaultProps} />);
|
||||
|
||||
// Initially local is selected
|
||||
const localBtn = screen.getByRole("button", { name: "Local" });
|
||||
const remoteBtn = screen.getByRole("button", { name: "Remote" });
|
||||
|
||||
expect(localBtn).toHaveAttribute("aria-pressed", "true");
|
||||
expect(remoteBtn).toHaveAttribute("aria-pressed", "false");
|
||||
|
||||
// Remote fields container should be hidden
|
||||
const remoteFieldsContainer = screen.getByTestId("remote-fields-container");
|
||||
expect(remoteFieldsContainer).toHaveAttribute("data-visible", "false");
|
||||
|
||||
// Switch to remote
|
||||
fireEvent.click(remoteBtn);
|
||||
|
||||
expect(localBtn).toHaveAttribute("aria-pressed", "false");
|
||||
expect(remoteBtn).toHaveAttribute("aria-pressed", "true");
|
||||
|
||||
// Remote fields container should be visible
|
||||
expect(remoteFieldsContainer).toHaveAttribute("data-visible", "true");
|
||||
|
||||
// URL and API Key fields should be visible
|
||||
expect(screen.getByPlaceholderText("https://node.example.com")).toBeInTheDocument();
|
||||
|
||||
// Switch back to local
|
||||
fireEvent.click(localBtn);
|
||||
|
||||
expect(localBtn).toHaveAttribute("aria-pressed", "true");
|
||||
expect(remoteBtn).toHaveAttribute("aria-pressed", "false");
|
||||
|
||||
// Remote fields container should be hidden again
|
||||
expect(remoteFieldsContainer).toHaveAttribute("data-visible", "false");
|
||||
});
|
||||
|
||||
it("submit button is disabled while submitting", async () => {
|
||||
let resolveSubmit: () => void;
|
||||
const slowSubmit = vi.fn().mockImplementation(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
resolveSubmit = resolve;
|
||||
})
|
||||
);
|
||||
|
||||
render(<AddNodeModal {...defaultProps} onSubmit={slowSubmit} />);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText("Build Machine"), {
|
||||
target: { value: "Test Node" },
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Add Node" }));
|
||||
|
||||
// Button should show "Adding..." and be disabled
|
||||
expect(screen.getByRole("button", { name: "Adding..." })).toBeDisabled();
|
||||
|
||||
// Resolve the submit
|
||||
resolveSubmit!();
|
||||
});
|
||||
|
||||
it("clicking overlay closes modal", () => {
|
||||
render(<AddNodeModal {...defaultProps} />);
|
||||
|
||||
// Click on the overlay (not the modal itself)
|
||||
const overlay = screen.getByRole("dialog").parentElement!;
|
||||
fireEvent.click(overlay);
|
||||
|
||||
expect(defaultProps.onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows hint text for max concurrent field", () => {
|
||||
render(<AddNodeModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText("Max simultaneous task agents (1–10)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows description text", () => {
|
||||
render(<AddNodeModal {...defaultProps} />);
|
||||
|
||||
expect(
|
||||
screen.getByText("Register a node to distribute task execution across machines.")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("submits remote node with URL and API key", async () => {
|
||||
render(<AddNodeModal {...defaultProps} />);
|
||||
|
||||
// Fill name
|
||||
fireEvent.change(screen.getByPlaceholderText("Build Machine"), {
|
||||
target: { value: "Remote Node" },
|
||||
});
|
||||
|
||||
// Switch to remote
|
||||
fireEvent.click(screen.getByRole("button", { name: "Remote" }));
|
||||
|
||||
// Fill URL and API key
|
||||
fireEvent.change(screen.getByPlaceholderText("https://node.example.com"), {
|
||||
target: { value: "https://node.example.com" },
|
||||
});
|
||||
fireEvent.change(screen.getByPlaceholderText("Optional"), {
|
||||
target: { value: "secret-key" },
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Add Node" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(defaultProps.onSubmit).toHaveBeenCalledWith({
|
||||
name: "Remote Node",
|
||||
type: "remote",
|
||||
url: "https://node.example.com",
|
||||
apiKey: "secret-key",
|
||||
maxConcurrent: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -27577,6 +27577,13 @@ html .column.drag-over * {
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.add-node-modal__description {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: var(--space-sm);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.add-node-modal__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -27584,13 +27591,12 @@ html .column.drag-over * {
|
||||
}
|
||||
|
||||
.add-node-modal__field > span {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.add-node-modal__field input,
|
||||
.add-node-modal__field select {
|
||||
.add-node-modal__field input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
@@ -27598,13 +27604,28 @@ html .column.drag-over * {
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.add-node-modal__field input:focus,
|
||||
.add-node-modal__field select:focus {
|
||||
.add-node-modal__field input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 20%, transparent);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent);
|
||||
}
|
||||
|
||||
.add-node-modal__field input[aria-invalid="true"] {
|
||||
border-color: var(--color-error);
|
||||
background: color-mix(in srgb, var(--color-error) 5%, var(--surface));
|
||||
}
|
||||
|
||||
.add-node-modal__field input[aria-invalid="true"]:focus {
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-error) 15%, transparent);
|
||||
}
|
||||
|
||||
.add-node-modal__hint {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.add-node-modal__error {
|
||||
@@ -27613,6 +27634,64 @@ html .column.drag-over * {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Type toggle (segmented control) */
|
||||
.add-node-modal__type-toggle {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.add-node-modal__type-btn {
|
||||
flex: 1;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
|
||||
.add-node-modal__type-btn:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--bg) 50%, var(--surface));
|
||||
}
|
||||
|
||||
.add-node-modal__type-btn:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: inset 0 0 0 2px var(--accent);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.add-node-modal__type-btn.active {
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
}
|
||||
|
||||
.add-node-modal__type-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Remote fields with animated show/hide */
|
||||
.add-node-modal__remote-fields {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
transition: grid-template-rows 0.2s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.add-node-modal__remote-fields[data-visible="true"] {
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.add-node-modal__remote-fields > * {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.node-detail-modal {
|
||||
max-width: 860px;
|
||||
width: min(860px, calc(100vw - 32px));
|
||||
@@ -27758,6 +27837,38 @@ html .column.drag-over * {
|
||||
.node-detail-modal__field--full {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
/* Add Node Modal mobile */
|
||||
.add-node-modal {
|
||||
width: calc(100vw - 16px);
|
||||
}
|
||||
|
||||
.add-node-modal__type-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.add-node-modal__type-btn {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.add-node-modal__field input {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
/* Instant transition on mobile for remote fields */
|
||||
.add-node-modal__remote-fields {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
/* Connect Node Modal mobile */
|
||||
.connect-node-modal {
|
||||
width: calc(100vw - 16px);
|
||||
}
|
||||
|
||||
.connect-node-field input {
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Mesh Topology ─────────────────────────────────────────────── */
|
||||
@@ -27833,53 +27944,6 @@ html .column.drag-over * {
|
||||
padding: var(--space-xl);
|
||||
}
|
||||
|
||||
/* ── Connect Node Modal ──────────────────────────────────────────── */
|
||||
|
||||
.connect-node-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.connect-node-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.connect-node-field label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.connect-node-field input {
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.connect-node-field input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.connect-node-field .error-message {
|
||||
font-size: 12px;
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.connect-node-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-sm);
|
||||
padding-top: var(--space-sm);
|
||||
}
|
||||
|
||||
/* ── Quick Chat FAB ──────────────────────────────────────────────── */
|
||||
|
||||
/* Position set via inline style from useDraggable */
|
||||
@@ -30057,9 +30121,9 @@ html .column.drag-over * {
|
||||
}
|
||||
|
||||
.connect-node-field > span {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.connect-node-field input {
|
||||
@@ -30069,12 +30133,22 @@ html .column.drag-over * {
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.connect-node-field input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 20%, transparent);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent);
|
||||
}
|
||||
|
||||
.connect-node-field input[aria-invalid="true"] {
|
||||
border-color: var(--color-error);
|
||||
background: color-mix(in srgb, var(--color-error) 5%, var(--surface));
|
||||
}
|
||||
|
||||
.connect-node-field input[aria-invalid="true"]:focus {
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-error) 15%, transparent);
|
||||
}
|
||||
|
||||
.connect-node-url-preview {
|
||||
@@ -30084,6 +30158,7 @@ html .column.drag-over * {
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
background: color-mix(in srgb, var(--surface) 50%, var(--bg));
|
||||
border-radius: var(--radius-sm);
|
||||
border-left: 3px solid var(--accent);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -30094,13 +30169,14 @@ html .column.drag-over * {
|
||||
.connect-node-url-preview code {
|
||||
font-family: var(--font-mono);
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.connect-node-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-sm);
|
||||
padding-top: var(--space-sm);
|
||||
padding: var(--modal-padding);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user