diff --git a/frontend/src/components/Common/Loading.tsx b/frontend/src/components/Common/Loading.tsx index 004333e..45bf845 100644 --- a/frontend/src/components/Common/Loading.tsx +++ b/frontend/src/components/Common/Loading.tsx @@ -1,4 +1,4 @@ -import { Loader2 } from "lucide-react" +import { FluksioLoader } from "@/components/ui/fluksio-loader" /** * The router's pending screen, shown while a page's code chunk is on its way. @@ -15,7 +15,7 @@ export function PageLoading() { data-testid="page-loading" className="flex min-h-64 items-center justify-center" > - + ) } diff --git a/frontend/src/components/Common/OverviewToolbar.tsx b/frontend/src/components/Common/OverviewToolbar.tsx index 593b5ff..6174650 100644 --- a/frontend/src/components/Common/OverviewToolbar.tsx +++ b/frontend/src/components/Common/OverviewToolbar.tsx @@ -1,5 +1,5 @@ import { useMutation } from "@tanstack/react-query" -import { Check, Loader2, Plus, Search, Trash2 } from "lucide-react" +import { Check, Plus, Search, Trash2 } from "lucide-react" import { motion } from "motion/react" import { type ReactNode, useRef, useState } from "react" @@ -13,6 +13,7 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog" +import { FluksioLoader } from "@/components/ui/fluksio-loader" import { Input } from "@/components/ui/input" import { Tooltip, @@ -170,7 +171,7 @@ export function OverviewToolbar({ aria-label="Publish all changes" data-testid="publish-all" > - {publishing ? : } + {publishing ? : } diff --git a/frontend/src/components/Dashboard/DashboardEditor.tsx b/frontend/src/components/Dashboard/DashboardEditor.tsx index 1b6a01e..97687cb 100644 --- a/frontend/src/components/Dashboard/DashboardEditor.tsx +++ b/frontend/src/components/Dashboard/DashboardEditor.tsx @@ -1,14 +1,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { useNavigate } from "@tanstack/react-router" -import { - Check, - ExternalLink, - Loader2, - Pencil, - Plus, - Settings2, - X, -} from "lucide-react" +import { Check, ExternalLink, Pencil, Plus, Settings2, X } from "lucide-react" import { motion } from "motion/react" import { type CSSProperties, useEffect, useRef, useState } from "react" import { @@ -36,6 +28,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog" +import { FluksioLoader } from "@/components/ui/fluksio-loader" import { Popover, PopoverContent, @@ -720,7 +713,7 @@ export function DashboardEditor({ data-testid="publish-dashboard" > {save.isPending || publish.isPending ? ( - + ) : ( )} diff --git a/frontend/src/components/Flow/FlowDock.tsx b/frontend/src/components/Flow/FlowDock.tsx index df6003f..297add5 100644 --- a/frontend/src/components/Flow/FlowDock.tsx +++ b/frontend/src/components/Flow/FlowDock.tsx @@ -2,7 +2,6 @@ import { useReactFlow } from "@xyflow/react" import { AlertCircle, Check, - Loader2, Maximize2, Pause, Pencil, @@ -18,6 +17,7 @@ import { useEffect, useState } from "react" import type { ValidationIssue } from "@/client" import { Button } from "@/components/ui/button" +import { FluksioLoader } from "@/components/ui/fluksio-loader" import { Separator } from "@/components/ui/separator" import { Tooltip, @@ -273,7 +273,7 @@ export function FlowDock({ data-testid="run-flow" > {running ? ( - + ) : ( )} @@ -348,7 +348,7 @@ export function FlowDock({ {!connected ? ( ) : saving || publishing ? ( - + ) : ( )} diff --git a/frontend/src/components/ui/fluksio-loader.tsx b/frontend/src/components/ui/fluksio-loader.tsx new file mode 100644 index 0000000..3492275 --- /dev/null +++ b/frontend/src/components/ui/fluksio-loader.tsx @@ -0,0 +1,44 @@ +/** + * FluksioLoader: the app's shared loading mark. + * + * The brand mark — four neurons in a ring — drawn as one continuous stroke + * that traces itself and wipes away in a loop (see the `.fluksio-loader-line` + * rule + `fluksio-draw` keyframes in index.css, which also guard + * `prefers-reduced-motion`: `MotionConfig` covers JS animations, not CSS ones). + * Renders a bare so it drops in for a lucide icon: size it with a + * Tailwind `size-*` class and color it via a `text-*` class (everything is + * currentColor). + */ + +/** + * The ring, clockwise from the top node: a 45° connector, then 270° around the + * node it lands on, four times over. Geometry from `assets/fluksio-light.svg` + * — same node centres and radius, with the connectors pulled onto the circles + * so the whole mark is a single subpath and the dash traces it in one lap. + * + * Stroke weight is the mark's own 1.5 rather than the weight 2 of the glyphs in + * `components/icons/`: at 2 the node rings close up into dots. + */ +const RING = + "M 13.71 5.84 L 18.16 10.29 A 2.424 2.424 0 1 1 18.16 13.71 L 13.71 18.16 A 2.424 2.424 0 1 1 10.29 18.16 L 5.84 13.71 A 2.424 2.424 0 1 1 5.84 10.29 L 10.29 5.84 A 2.424 2.424 0 1 1 13.71 5.84 Z" + +export function FluksioLoader({ + className, + ...props +}: React.SVGProps) { + return ( + + + + ) +} diff --git a/frontend/src/components/ui/loading-button.tsx b/frontend/src/components/ui/loading-button.tsx index 46cc2aa..ea78b4a 100644 --- a/frontend/src/components/ui/loading-button.tsx +++ b/frontend/src/components/ui/loading-button.tsx @@ -1,6 +1,6 @@ import { Slot, Slottable } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority" -import { Loader2 } from "lucide-react" +import { FluksioLoader } from "@/components/ui/fluksio-loader" import { cn } from "@/lib/utils" const buttonVariants = cva( @@ -58,7 +58,7 @@ function LoadingButton({ disabled={loading || disabled} {...props} > - {loading && } + {loading && } {children} ) diff --git a/frontend/src/index.css b/frontend/src/index.css index 9c9a2ec..1c2a116 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -211,3 +211,36 @@ font-family: "Inter", "Helvetica Neue", Arial, sans-serif; letter-spacing: -0.01em; } + +/* ── Loading mark (FluksioLoader) ───────────────────────────────────── */ +/* + * The brand ring is a single subpath with `pathLength="1"`, so a `1 1` dash is + * exactly one lap of it: offset 1 hides the mark, 0 completes it, and -1 wipes + * it off the same end it was drawn from. The hold in the middle is what makes + * the mark legible once per lap instead of only flashing through. + */ +@keyframes fluksio-draw { + 0% { + stroke-dashoffset: 1; + } + 45%, + 55% { + stroke-dashoffset: 0; + } + 100% { + stroke-dashoffset: -1; + } +} + +.fluksio-loader-line { + stroke-dasharray: 1 1; + animation: fluksio-draw 1.6s var(--ease-standard) infinite; +} + +/* `MotionConfig reducedMotion="user"` reaches JS animations only. */ +@media (prefers-reduced-motion: reduce) { + .fluksio-loader-line { + stroke-dasharray: none; + animation: none; + } +}