@umituz/web-dashboard 6.7.0 → 6.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/web-dashboard",
3
- "version": "6.7.0",
3
+ "version": "6.8.0",
4
4
  "description": "Installable app skeleton for the fleet — layouts, analytics (dependency-free SVG charts), calendar, onboarding flow, i18n via @umituz/web-localization, permissions/module registry; udb-* stylesheet on design-system tokens",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -66,7 +66,7 @@
66
66
  }
67
67
  },
68
68
  "dependencies": {
69
- "@umituz/web-design-system": "^4.18.0",
69
+ "@umituz/web-design-system": "^4.19.0",
70
70
  "lucide-react": "^0.577.0",
71
71
  "@umituz/web-localization": "^1.2.1"
72
72
  },
@@ -1,5 +1,5 @@
1
- import { describe, expect, it } from "vitest";
2
- import { render, screen } from "@testing-library/react";
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
3
  import { MemoryRouter, Routes, Route } from "react-router-dom";
4
4
  import { Home } from "lucide-react";
5
5
  import { DashboardLayout } from "./DashboardLayout";
@@ -16,14 +16,14 @@ const config: DashboardLayoutConfig = {
16
16
  defaultTitle: "Workspace",
17
17
  };
18
18
 
19
- function renderAt(path: string, mainId?: string) {
19
+ function renderAt(path: string, mainId?: string, configOverride?: DashboardLayoutConfig) {
20
20
  return render(
21
21
  <MemoryRouter initialEntries={[path]}>
22
22
  <Routes>
23
23
  <Route
24
24
  path="*"
25
25
  element={
26
- <DashboardLayout config={config} mainId={mainId}>
26
+ <DashboardLayout config={configOverride ?? config} mainId={mainId}>
27
27
  {/* children are ignored — the layout renders an Outlet */}
28
28
  </DashboardLayout>
29
29
  }
@@ -55,3 +55,47 @@ describe("DashboardLayout (shell a11y)", () => {
55
55
  expect(main?.textContent).toContain("outlet-content");
56
56
  });
57
57
  });
58
+
59
+ describe("DashboardLayout (command palette)", () => {
60
+ const paletteConfig = (extra?: Partial<DashboardLayoutConfig>): DashboardLayoutConfig => ({
61
+ ...config,
62
+ commandPalette: {
63
+ groups: [{ heading: "Go to", items: [{ id: "home", label: "Home", onSelect: () => {} }] }],
64
+ placeholder: "Search commands…",
65
+ },
66
+ ...extra,
67
+ });
68
+
69
+ it("renders the palette's search button in the header and opens it on click", async () => {
70
+ renderAt("/", undefined, paletteConfig());
71
+ fireEvent.click(screen.getByRole("button", { name: /search commands/i }));
72
+ expect(await screen.findByRole("dialog")).toBeTruthy();
73
+ expect(await screen.findByRole("option", { name: "Home" })).toBeTruthy();
74
+ });
75
+
76
+ it("toggles the palette with ⌘K / Ctrl+K", async () => {
77
+ renderAt("/", undefined, paletteConfig());
78
+ expect(screen.queryByRole("dialog")).toBeNull();
79
+ fireEvent.keyDown(window, { key: "k", metaKey: true });
80
+ expect(await screen.findByRole("dialog")).toBeTruthy();
81
+ fireEvent.keyDown(window, { key: "k", metaKey: true });
82
+ expect(screen.queryByRole("dialog")).toBeNull();
83
+ });
84
+
85
+ it("does not bind the shortcut when shortcut: false", () => {
86
+ renderAt("/", undefined, {
87
+ ...config,
88
+ commandPalette: { groups: [], shortcut: false },
89
+ });
90
+ fireEvent.keyDown(window, { key: "k", ctrlKey: true });
91
+ expect(screen.queryByRole("dialog")).toBeNull();
92
+ });
93
+
94
+ it("keeps an explicit searchAction.onActivate over the palette takeover", () => {
95
+ const onActivate = vi.fn();
96
+ renderAt("/", undefined, paletteConfig({ searchAction: { placeholder: "Go search", onActivate } }));
97
+ fireEvent.click(screen.getByRole("button", { name: /go search/i }));
98
+ expect(onActivate).toHaveBeenCalledTimes(1);
99
+ expect(screen.queryByRole("dialog")).toBeNull();
100
+ });
101
+ });
@@ -2,6 +2,7 @@ import { useState, useEffect, type ReactNode } from "react";
2
2
  import { useLocation, Outlet, Navigate } from "react-router-dom";
3
3
  import { useT } from "@umituz/web-localization/react";
4
4
  import { Skeleton } from "@umituz/web-design-system/atoms";
5
+ import { CommandPalette } from "@umituz/web-design-system/organisms";
5
6
  import { DashboardSidebar } from "./DashboardSidebar";
6
7
  import { DashboardHeader } from "./DashboardHeader";
7
8
  import { getActiveSidebarItem } from "../utils/dashboard";
@@ -64,6 +65,8 @@ export const DashboardLayout = ({
64
65
  const [collapsed, setCollapsed] = useState(() => readCollapsed(collapsedStorageKey));
65
66
  const [mobileOpen, setMobileOpen] = useState(false);
66
67
  const [loading, setLoading] = useState(routeTransitionMs > 0);
68
+ const [paletteOpen, setPaletteOpen] = useState(false);
69
+ const palette = config.commandPalette;
67
70
 
68
71
  useEffect(() => {
69
72
  if (!collapsedStorageKey || typeof window === "undefined") return;
@@ -88,6 +91,18 @@ export const DashboardLayout = ({
88
91
  setMobileOpen(false);
89
92
  }, [location.pathname]);
90
93
 
94
+ useEffect(() => {
95
+ if (!palette || palette.shortcut === false) return;
96
+ const onKeyDown = (e: KeyboardEvent) => {
97
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
98
+ e.preventDefault();
99
+ setPaletteOpen((v) => !v);
100
+ }
101
+ };
102
+ window.addEventListener("keydown", onKeyDown);
103
+ return () => window.removeEventListener("keydown", onKeyDown);
104
+ }, [palette]);
105
+
91
106
  useEffect(() => {
92
107
  if (!mobileOpen) return;
93
108
  const onKeyDown = (e: KeyboardEvent) => {
@@ -175,7 +190,12 @@ export const DashboardLayout = ({
175
190
  onMarkAllRead={onMarkAllRead}
176
191
  onDismissNotification={onDismissNotification}
177
192
  loginRoute={loginRoute}
178
- searchAction={config.searchAction}
193
+ searchAction={
194
+ palette
195
+ ? // The palette owns the header search unless the app gave an explicit action.
196
+ { placeholder: palette.placeholder, shortcut: "⌘K", ...config.searchAction, onActivate: config.searchAction?.onActivate ?? (() => setPaletteOpen(true)) }
197
+ : config.searchAction
198
+ }
179
199
  headerExtra={config.headerExtra}
180
200
  profileItems={config.profileItems}
181
201
  showDefaultProfileItems={config.showDefaultProfileItems}
@@ -200,6 +220,17 @@ export const DashboardLayout = ({
200
220
 
201
221
  {footer && <footer className="udb-layout__footer">{footer}</footer>}
202
222
  </div>
223
+
224
+ {palette && (
225
+ <CommandPalette
226
+ open={paletteOpen}
227
+ onOpenChange={setPaletteOpen}
228
+ groups={palette.groups}
229
+ placeholder={palette.placeholder}
230
+ emptyLabel={palette.emptyLabel}
231
+ label={palette.label}
232
+ />
233
+ )}
203
234
  </div>
204
235
  );
205
236
  };
@@ -32,6 +32,18 @@ export interface HeaderSearchAction {
32
32
  onActivate: () => void;
33
33
  }
34
34
 
35
+ /** ⌘K command palette wired into the shell: renders the DS CommandPalette,
36
+ * binds ⌘K/Ctrl+K, and (unless searchAction overrides it) makes the header
37
+ * search button open the palette. */
38
+ export interface CommandPaletteConfig {
39
+ groups: import("@umituz/web-design-system/organisms").CommandGroup[];
40
+ placeholder?: string;
41
+ emptyLabel?: string;
42
+ label?: string;
43
+ /** Bind ⌘K/Ctrl+K to open the palette (default true). */
44
+ shortcut?: boolean;
45
+ }
46
+
35
47
  export interface DashboardSidebarProps {
36
48
  collapsed: boolean;
37
49
  setCollapsed: (collapsed: boolean) => void;
@@ -51,6 +63,9 @@ export interface DashboardLayoutConfig {
51
63
  /** Extra node rendered in the header actions area (app chips, clocks…). */
52
64
  headerExtra?: React.ReactNode;
53
65
  searchAction?: HeaderSearchAction;
66
+ /** ⌘K palette — when set, the header search opens it (an explicit
67
+ * searchAction.onActivate still wins). */
68
+ commandPalette?: CommandPaletteConfig;
54
69
  /** Fully custom profile menu items — shown in addition to (or, with showDefaultProfileItems=false, instead of) the defaults. */
55
70
  profileItems?: HeaderProfileItem[];
56
71
  /** Hide the built-in profile/settings items (defaults to true). */