diff --git a/NOTEPAD.md b/NOTEPAD.md index 3e1e2fe..ed19531 100644 --- a/NOTEPAD.md +++ b/NOTEPAD.md @@ -8,10 +8,11 @@ Deferring because out of scope is fine, but don't mention deferring than. ## Open -- BUG/UI: the CSS duration tokens were retuned to 250/500 ms but `lib/motion.ts` still - carries 200/300, so JS animations (panels, dock, tabs) run at a different speed to CSS - transitions. They are meant to mirror each other; pick the intended values and update - `motion.ts` in both repos. +- BUG/UI: the sidebar background on dark mode does not match the background of the viewport/flow panel +- BUG/UI: there is some strage dot (li item) sitting next to the appearance button +- FEAT/UI: introduce User settings page showing up in the sidebar where the apperance, email password etc can be set (copy UX design from ../../n3xd/app) +- BUG/UI: edge value labels should be opaque +- BUG/UI: the popover which opens when clicking on an edge should be more compact, i.e. make label, value and last updated fit in a single row (truncate decimals) - BUG/API: `pytest tests/` deletes every user on teardown (`tests/conftest.py`), so running it against the development database logs you out of the running app. Point tests at their own database, or reseed with `init_db` afterwards. diff --git a/frontend/src/components/Flow/FlowEditor.tsx b/frontend/src/components/Flow/FlowEditor.tsx index aeb634d..8b1572f 100644 --- a/frontend/src/components/Flow/FlowEditor.tsx +++ b/frontend/src/components/Flow/FlowEditor.tsx @@ -121,6 +121,8 @@ function FlowEditorInner({ flowName }: { flowName: string }) { const [inspected, setInspected] = useState(null) const [rebind, setRebind] = useState(null) const [flowPanelOpen, setFlowPanelOpen] = useState(false) + // The editor at full size covers the canvas, so its chrome steps aside. + const [editorExpanded, setEditorExpanded] = useState(false) const issues = detail.issues ?? [] @@ -234,7 +236,10 @@ function FlowEditorInner({ flowName }: { flowName: string }) { const renameMutation = useMutation({ mutationFn: (newName: string) => - FlowsService.renameFlow({ name: flowName, requestBody: { new_name: newName } }), + FlowsService.renameFlow({ + name: flowName, + requestBody: { new_name: newName }, + }), onSuccess: (detail) => { queryClient.invalidateQueries({ queryKey: flowKeys.all }) setFlowPanelOpen(false) @@ -442,6 +447,7 @@ function FlowEditorInner({ flowName }: { flowName: string }) { }} onPaneClick={() => { setSelectedId(null) + setEditorExpanded(false) setInspected(null) }} onEdgeClick={(event, edge) => { @@ -476,26 +482,30 @@ function FlowEditorInner({ flowName }: { flowName: string }) { - + {editorExpanded ? null : ( + + )} - setPaletteOpen(true)} - onEditFlow={() => { - setSelectedId(null) - setFlowPanelOpen(true) - }} - onRun={() => { - flush() - runMutation.mutate() - }} - onFocusNode={focusNode} - /> + {editorExpanded ? null : ( + setPaletteOpen(true)} + onEditFlow={() => { + setSelectedId(null) + setFlowPanelOpen(true) + }} + onRun={() => { + flush() + runMutation.mutate() + }} + onFocusNode={focusNode} + /> + )} {definitions.length === 0 ? (
@@ -534,12 +544,15 @@ function FlowEditorInner({ flowName }: { flowName: string }) { flow={flowName} nodeTypes={nodeTypeInfo ?? []} suggestions={suggestions} + expanded={editorExpanded} + onToggleExpand={() => setEditorExpanded((wide) => !wide)} onChange={updateNode} onSaveSource={(code) => { if (selected) sourceMutation.mutate({ nodeId: selected.id, code }) }} onClose={() => { flush() + setEditorExpanded(false) setSelectedId(null) }} onDelete={() => selected && deleteNodes([selected.id])} diff --git a/frontend/src/components/Flow/LiveEdge.tsx b/frontend/src/components/Flow/LiveEdge.tsx index 8b47c91..45d3bf3 100644 --- a/frontend/src/components/Flow/LiveEdge.tsx +++ b/frontend/src/components/Flow/LiveEdge.tsx @@ -7,6 +7,7 @@ import { } from "@xyflow/react" import { memo, useEffect, useRef, useState } from "react" +import { duration } from "@/lib/motion" import { cn } from "@/lib/utils" import type { FlowEdgeData } from "./deriveEdges" import { useLiveValue } from "./liveStore" @@ -53,7 +54,7 @@ function LiveEdgeComponent({ if (!live?.ts || live.ts === lastTs.current) return lastTs.current = live.ts setPulsing(true) - const timer = setTimeout(() => setPulsing(false), 300) + const timer = setTimeout(() => setPulsing(false), duration.pulse * 1000) return () => clearTimeout(timer) }, [live?.ts]) diff --git a/frontend/src/components/Flow/NodePanel.tsx b/frontend/src/components/Flow/NodePanel.tsx index 881aa10..38c2896 100644 --- a/frontend/src/components/Flow/NodePanel.tsx +++ b/frontend/src/components/Flow/NodePanel.tsx @@ -1,5 +1,5 @@ import { useQuery } from "@tanstack/react-query" -import { X } from "lucide-react" +import { Maximize2, Minimize2, X } from "lucide-react" import { lazy, Suspense, useEffect, useRef, useState } from "react" import type { DType, MessageSpec, NodeDef_Input, NodeTypeInfo } from "@/client" @@ -22,6 +22,7 @@ import { SelectValue, } from "@/components/ui/select" import { Switch } from "@/components/ui/switch" +import { cn } from "@/lib/utils" import { nodeSourceQueryOptions } from "./queries" import { PANEL_SECTION, SidePanel } from "./SidePanel" @@ -276,15 +277,19 @@ function PanelBody({ flow, nodeType, suggestions, + expanded, onChange, onSaveSource, + onToggleExpand, }: { node: NodeDef_Input flow: string nodeType: NodeTypeInfo | undefined suggestions: PortSuggestions + expanded: boolean onChange: (next: NodeDef_Input) => void onSaveSource: (code: string) => void + onToggleExpand: () => void }) { const hasSource = nodeType?.has_source ?? node.type === "python" const { data: source } = useQuery({ @@ -320,7 +325,7 @@ function PanelBody({ return ( <> -
+
- Code +
+ Code + +
void onSaveSource: (code: string) => void + onToggleExpand: () => void onClose: () => void onDelete: () => void }) { @@ -399,6 +422,7 @@ export function NodePanel({ label="Node settings" testId="node-panel" bodyKey={node?.id ?? "none"} + expanded={expanded} onClose={onClose} header={ node ? ( @@ -429,8 +453,10 @@ export function NodePanel({ flow={flow} nodeType={nodeType} suggestions={suggestions} + expanded={expanded} onChange={onChange} onSaveSource={onSaveSource} + onToggleExpand={onToggleExpand} /> ) : null} diff --git a/frontend/src/components/Flow/SidePanel.tsx b/frontend/src/components/Flow/SidePanel.tsx index f0a1e25..27fc9e4 100644 --- a/frontend/src/components/Flow/SidePanel.tsx +++ b/frontend/src/components/Flow/SidePanel.tsx @@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button" import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet" import { useIsMobile } from "@/hooks/useMobile" import { duration, easeEmphasized, easeStandard } from "@/lib/motion" +import { cn } from "@/lib/utils" /** Same grammar as the shared `slideUp`, on the axis this panel travels. */ const panelSlide = { @@ -38,6 +39,7 @@ export function SidePanel({ label, testId, bodyKey, + expanded = false, header, footer, children, @@ -49,6 +51,8 @@ export function SidePanel({ testId: string /** Remounts the contents when the thing being edited changes. */ bodyKey: string + /** Fill the content area instead of floating beside the canvas. */ + expanded?: boolean header: ReactNode footer?: ReactNode children: ReactNode @@ -125,7 +129,14 @@ export function SidePanel({ role="complementary" aria-label={label} data-testid={testId} - className="pointer-events-auto absolute inset-y-4 right-4 z-10 flex w-[400px] flex-col overflow-hidden rounded-lg border border-border bg-card/80 shadow-e2 backdrop-blur-md" + data-expanded={expanded || undefined} + className={cn( + "pointer-events-auto absolute z-20 flex flex-col overflow-hidden border border-border bg-card/80 shadow-e2 backdrop-blur-md", + expanded + ? // Fills the content region, which already starts after the sidebar. + "inset-0 rounded-none" + : "inset-y-4 right-4 w-[400px] rounded-lg", + )} > {contents} diff --git a/frontend/src/components/Flow/flow.css b/frontend/src/components/Flow/flow.css index ba8220c..5428cd4 100644 --- a/frontend/src/components/Flow/flow.css +++ b/frontend/src/components/Flow/flow.css @@ -34,7 +34,7 @@ /* A message arriving lights its edge, then decays back to rest. */ @media (prefers-reduced-motion: no-preference) { .react-flow__edge-path.edge-live { - animation: edge-pulse var(--duration-slow) var(--ease-emphasized); + animation: edge-pulse var(--duration-pulse) var(--ease-emphasized); } @keyframes edge-pulse { @@ -58,7 +58,7 @@ border: 2px solid var(--primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent); pointer-events: none; - animation: node-pulse var(--duration-slow) var(--ease-emphasized) forwards; + animation: node-pulse var(--duration-pulse) var(--ease-emphasized) forwards; } @keyframes node-pulse { diff --git a/frontend/src/index.css b/frontend/src/index.css index b8307e7..5913a02 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -35,8 +35,11 @@ --ease-emphasized: cubic-bezier(0.2, 0, 0, 1); --ease-standard: cubic-bezier(0.4, 0, 0.2, 1); --duration-fast: 150ms; - --duration-base: 250ms; - --duration-slow: 500ms; + --duration-base: 200ms; + --duration-slow: 300ms; + /* One-shot feedback (a message arriving, a node emitting) rather than a UI + state change, so it lingers long enough to be noticed. */ + --duration-pulse: 500ms; --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); diff --git a/frontend/src/lib/motion.ts b/frontend/src/lib/motion.ts index f8a85dc..739b423 100644 --- a/frontend/src/lib/motion.ts +++ b/frontend/src/lib/motion.ts @@ -19,7 +19,13 @@ export const easeEmphasized: [number, number, number, number] = [0.2, 0, 0, 1] export const easeStandard: [number, number, number, number] = [0.4, 0, 0.2, 1] /** Durations in seconds, mirroring the `--duration-*` tokens (ms). */ -export const duration = { fast: 0.15, base: 0.2, slow: 0.3 } as const +export const duration = { + fast: 0.15, + base: 0.2, + slow: 0.3, + /** One-shot feedback, not a state change. See `--duration-pulse`. */ + pulse: 0.5, +} as const export const transitions = { /** Enter / expressive moves (decelerate). */