From 9c75d7ae133537a68bb96e05865b43b853bac905 Mon Sep 17 00:00:00 2001 From: stroblme Date: Mon, 12 Oct 2026 21:59:16 +0200 Subject: [PATCH] =?UTF-8?q?Dashboard=20add=20widget:=20searchable=20comman?= =?UTF-8?q?d=20palette=20with=20=E2=8C=98P,=20as=20on=20flows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../components/Dashboard/DashboardEditor.tsx | 54 +++++++------- .../components/Dashboard/WidgetPalette.tsx | 71 +++++++++++++++++++ frontend/src/components/Dashboard/widgets.tsx | 24 +++++++ 3 files changed, 119 insertions(+), 30 deletions(-) create mode 100644 frontend/src/components/Dashboard/WidgetPalette.tsx diff --git a/frontend/src/components/Dashboard/DashboardEditor.tsx b/frontend/src/components/Dashboard/DashboardEditor.tsx index b5bb194..190daf7 100644 --- a/frontend/src/components/Dashboard/DashboardEditor.tsx +++ b/frontend/src/components/Dashboard/DashboardEditor.tsx @@ -29,11 +29,6 @@ import { DialogTitle, } from "@/components/ui/dialog" import { FluksioLoader } from "@/components/ui/fluksio-loader" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover" import { Separator } from "@/components/ui/separator" import { Tooltip, @@ -44,6 +39,7 @@ import useCustomToast from "@/hooks/useCustomToast" import { useIsMobile } from "@/hooks/useMobile" import { slideUp, transitions } from "@/lib/motion" import { appPath } from "@/lib/portal" +import { useShortcuts } from "@/lib/shortcuts" import { cn } from "@/lib/utils" import { handleError } from "@/utils" import { @@ -73,6 +69,7 @@ import { PaletteProvider } from "./settings" import { useUi } from "./ui" import { showTitle } from "./ui/core/config" import { LookProvider, useCanvasRoot } from "./ui/core/look" +import { WidgetPalette } from "./WidgetPalette" import { WIDGET_LABELS, WIDGET_SIZES, @@ -81,8 +78,6 @@ import { widgetIssue, } from "./widgets" -const KINDS = Object.keys(WIDGET_LABELS) as WidgetKind[] - /** * A widget as the editor draws it: the panel's own look, plus a click that * picks it. @@ -240,6 +235,7 @@ export function DashboardEditor({ const [draft, setDraft] = useState(dashboard) const [selected, setSelected] = useState(null) const [settingsOpen, setSettingsOpen] = useState(false) + const [paletteOpen, setPaletteOpen] = useState(false) // A phone reads the dashboard rather than arranges it, so the grid library // never mounts there. See DESIGN-GUIDELINES.md → Responsive. const stacked = useIsMobile() @@ -342,6 +338,13 @@ export function DashboardEditor({ setSelected(id) } + // ⌘P, as on the flow canvas. Reachable while typing, so a settings field + // having focus does not hide it. + useShortcuts( + { "mod+p": () => (edit ? setPaletteOpen((open) => !open) : false) }, + ["mod+p"], + ) + const patch = (id: string, changes: Partial) => updateWidgets( widgets.map((widget) => @@ -457,7 +460,8 @@ export function DashboardEditor({ className="text-sm text-muted-foreground" data-testid="dashboard-empty" > - Nothing on this dashboard yet. Add a widget from the bar below. + Nothing on this dashboard yet. Press ⌘P, or use the plus in the bar + below.

) : stacked ? ( // One column at the viewport's width. Edit mode still picks a widget and @@ -622,37 +626,21 @@ export function DashboardEditor({ ) : null} - - + + - - -

- Add a widget -

-
- {KINDS.map((kind) => ( - - ))} -
-
-
+ + Add a widget (⌘P) + @@ -785,6 +773,12 @@ export function DashboardEditor({ {edit ? ( <> + + active && patch(active.id, changes)} diff --git a/frontend/src/components/Dashboard/WidgetPalette.tsx b/frontend/src/components/Dashboard/WidgetPalette.tsx new file mode 100644 index 0000000..f1498b1 --- /dev/null +++ b/frontend/src/components/Dashboard/WidgetPalette.tsx @@ -0,0 +1,71 @@ +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command" +import { + INPUT_WIDGETS, + WIDGET_DESCRIPTIONS, + WIDGET_LABELS, + type WidgetKind, +} from "./widgets" + +const KINDS = Object.keys(WIDGET_LABELS) as WidgetKind[] + +const GROUPS = [ + ["Show a value", KINDS.filter((kind) => !INPUT_WIDGETS.has(kind))], + ["Control something", KINDS.filter((kind) => INPUT_WIDGETS.has(kind))], +] as const + +/** ⌘P on a dashboard being edited: the flow canvas's add-a-node palette, for widgets. */ +export function WidgetPalette({ + open, + onOpenChange, + onAdd, +}: { + open: boolean + onOpenChange: (open: boolean) => void + onAdd: (kind: WidgetKind) => void +}) { + // Unmounted rather than faded, for the reason the flow palette gives. + if (!open) return null + + return ( + + + + Nothing matches that. + {GROUPS.map(([heading, kinds]) => ( + + {kinds.map((kind) => ( + { + onOpenChange(false) + onAdd(kind) + }} + > + + {WIDGET_LABELS[kind]} + + {WIDGET_DESCRIPTIONS[kind]} + + + + ))} + + ))} + + + ) +} diff --git a/frontend/src/components/Dashboard/widgets.tsx b/frontend/src/components/Dashboard/widgets.tsx index 4968329..03dc8ec 100644 --- a/frontend/src/components/Dashboard/widgets.tsx +++ b/frontend/src/components/Dashboard/widgets.tsx @@ -97,6 +97,30 @@ export const WIDGET_LABELS: Record = { color: "Colour", } +/** One line per type for the add palette, which searches these too. */ +export const WIDGET_DESCRIPTIONS: Record = { + stat: "A single reading, large", + gauge: "A number against its range", + chart: "Readings over time", + markdown: "Formatted text, headings and links", + agenda: "Upcoming events from a calendar list", + notification: "The latest published title and message", + bar: "Several readings, a bar each", + icon: "An icon picked by the value it reads", + forecast: "What comes next, as a strip of columns", + clock: "Local time and date", + media: "A camera image, clip or audio", + player: "What a streamer is playing", + embed: "Another web page, framed", + button: "Sends a value when pressed", + buttons: "A grid of buttons, each its own value", + switch: "Turns something on or off", + slider: "Sets a number by dragging", + input: "Sends what is typed", + dropdown: "Picks one of a list of options", + color: "Picks a colour or brightness", +} + /** Default footprint per type, in grid units. */ export const WIDGET_SIZES: Record = { stat: { w: 3, h: 2 },