@iloveagents/foundry-web-ui 0.1.3 → 0.1.5
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/CHANGELOG.md +18 -0
- package/package.json +3 -3
- package/src/components/sidebar.test.tsx +83 -1
- package/src/components/sidebar.tsx +43 -0
- package/src/lib/nav-config.ts +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @iloveagents/foundry-web-ui
|
|
2
2
|
|
|
3
|
+
## 0.1.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f9144ca: Add `NavItem.statusIcon` slot for sidebar warning / status hints.
|
|
8
|
+
|
|
9
|
+
Renders a small tone-coloured icon next to the existing `statusDot`. Use for "this item needs attention" annotations (failed background jobs, permission drift, etc.) where a coloured dot isn't legible enough. Tones map to `warning` / `danger` / `info` / `neutral`. The `label` lands on both `aria-label` and the wrapping span's `title` attribute so screen-reader announcement + hover tooltip work without consumer wiring.
|
|
10
|
+
- @iloveagents/foundry-agent@0.1.5
|
|
11
|
+
- @iloveagents/foundry-web-primitives@0.1.5
|
|
12
|
+
|
|
13
|
+
## 0.1.4
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- Updated dependencies [07ace7e]
|
|
18
|
+
- @iloveagents/foundry-agent@0.1.4
|
|
19
|
+
- @iloveagents/foundry-web-primitives@0.1.4
|
|
20
|
+
|
|
3
21
|
## 0.1.3
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iloveagents/foundry-web-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"lucide-react": ">=0.400.0",
|
|
24
24
|
"@azure/msal-browser": "^5.0.0",
|
|
25
25
|
"@azure/msal-react": "^5.0.0",
|
|
26
|
-
"@iloveagents/foundry-agent": "0.1.
|
|
27
|
-
"@iloveagents/foundry-web-primitives": "0.1.
|
|
26
|
+
"@iloveagents/foundry-agent": "0.1.5",
|
|
27
|
+
"@iloveagents/foundry-web-primitives": "0.1.5"
|
|
28
28
|
},
|
|
29
29
|
"peerDependenciesMeta": {
|
|
30
30
|
"@azure/msal-browser": {
|
|
@@ -2,7 +2,7 @@ import { act } from "react";
|
|
|
2
2
|
import { createRoot, type Root } from "react-dom/client";
|
|
3
3
|
import { MemoryRouter } from "react-router";
|
|
4
4
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
5
|
-
import { FolderOpen, MessageSquare } from "lucide-react";
|
|
5
|
+
import { AlertTriangle, FolderOpen, MessageSquare } from "lucide-react";
|
|
6
6
|
import { useNavStore } from "../lib/nav-store.ts";
|
|
7
7
|
import { useAppStore } from "../lib/app-store.ts";
|
|
8
8
|
import { SIDEBAR_WIDTH, useSidebarStore } from "../lib/sidebar-store.ts";
|
|
@@ -323,4 +323,86 @@ describe("Sidebar active state", () => {
|
|
|
323
323
|
expect(wrappers[1].className).toContain("border-t");
|
|
324
324
|
}
|
|
325
325
|
});
|
|
326
|
+
|
|
327
|
+
it("renders NavItem.statusIcon with tone class + tooltip label", async () => {
|
|
328
|
+
// The statusIcon slot is consumer-driven (e.g. ``spaces-web-ui``
|
|
329
|
+
// stamps a warning triangle on workspaces with failed indexing).
|
|
330
|
+
// This test pins three things the consumer relies on:
|
|
331
|
+
// 1. The supplied LucideIcon component is rendered.
|
|
332
|
+
// 2. The tone maps to the right Tailwind class (``warning`` →
|
|
333
|
+
// ``text-warning``).
|
|
334
|
+
// 3. The ``label`` lands on both ``aria-label`` and the
|
|
335
|
+
// ``title`` attribute so the icon gets a hover tooltip and
|
|
336
|
+
// a screen-reader announcement without the consumer having
|
|
337
|
+
// to wire anything else.
|
|
338
|
+
const { Sidebar } = await import("./sidebar.tsx");
|
|
339
|
+
|
|
340
|
+
const navConfig: NavGroup[] = [
|
|
341
|
+
{
|
|
342
|
+
label: "Workspaces",
|
|
343
|
+
items: [
|
|
344
|
+
{
|
|
345
|
+
to: "/spaces/wsA",
|
|
346
|
+
label: "Workspace With Failures",
|
|
347
|
+
icon: FolderOpen,
|
|
348
|
+
statusIcon: {
|
|
349
|
+
icon: AlertTriangle,
|
|
350
|
+
tone: "warning",
|
|
351
|
+
label: "3 documents failed to index",
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
to: "/spaces/wsB",
|
|
356
|
+
label: "Healthy Workspace",
|
|
357
|
+
icon: FolderOpen,
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
},
|
|
361
|
+
];
|
|
362
|
+
useNavStore.getState().setConfig(navConfig);
|
|
363
|
+
useAppStore.setState({
|
|
364
|
+
currentPage: "/spaces/wsA",
|
|
365
|
+
threadActive: false,
|
|
366
|
+
contextItems: [],
|
|
367
|
+
sentContext: {},
|
|
368
|
+
highlights: [],
|
|
369
|
+
selectedItemId: null,
|
|
370
|
+
content: {},
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
await act(async () => {
|
|
374
|
+
root.render(
|
|
375
|
+
<MemoryRouter initialEntries={["/spaces/wsA"]}>
|
|
376
|
+
<TooltipProvider>
|
|
377
|
+
<Sidebar />
|
|
378
|
+
</TooltipProvider>
|
|
379
|
+
</MemoryRouter>,
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// The warning row carries the icon wrapped in a span. The label
|
|
384
|
+
// lands on both ``aria-label`` (screen reader) and ``title``
|
|
385
|
+
// (hover tooltip) on the wrapping span; the inner svg carries
|
|
386
|
+
// the tone class.
|
|
387
|
+
const warningWrappers = container.querySelectorAll<HTMLElement>(
|
|
388
|
+
'span[aria-label="3 documents failed to index"]',
|
|
389
|
+
);
|
|
390
|
+
expect(warningWrappers.length).toBeGreaterThan(0);
|
|
391
|
+
const wrapper = warningWrappers[0];
|
|
392
|
+
expect(wrapper.getAttribute("title")).toBe("3 documents failed to index");
|
|
393
|
+
expect(wrapper.querySelector("svg.text-warning")).not.toBeNull();
|
|
394
|
+
|
|
395
|
+
// No statusIcon on the healthy workspace ⇒ its leaf row
|
|
396
|
+
// contains no warning svg. Workspaces are rendered as NavLink
|
|
397
|
+
// anchors (button is for folders), so scope to ``a, button``
|
|
398
|
+
// and reject ancestor containers that happen to enclose BOTH
|
|
399
|
+
// workspaces.
|
|
400
|
+
const healthyRowLeaves = Array.from(
|
|
401
|
+
container.querySelectorAll<HTMLElement>("a, button"),
|
|
402
|
+
).filter((el) => el.textContent?.includes("Healthy Workspace"));
|
|
403
|
+
expect(healthyRowLeaves.length).toBeGreaterThan(0);
|
|
404
|
+
for (const leaf of healthyRowLeaves) {
|
|
405
|
+
expect(leaf.querySelectorAll("svg.text-warning").length).toBe(0);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
326
408
|
});
|
|
@@ -28,6 +28,7 @@ const ACTIVE_NAV_BADGE_CLASS =
|
|
|
28
28
|
const NESTED_NAV_INDENTS = ["pl-5", "pl-7", "pl-9", "pl-11", "pl-13"] as const;
|
|
29
29
|
|
|
30
30
|
type NavStatusDot = NonNullable<NavItem["statusDot"]>;
|
|
31
|
+
type NavStatusIcon = NonNullable<NavItem["statusIcon"]>;
|
|
31
32
|
|
|
32
33
|
function getNestedNavIndent(depth: number) {
|
|
33
34
|
return NESTED_NAV_INDENTS[depth] ?? NESTED_NAV_INDENTS[NESTED_NAV_INDENTS.length - 1];
|
|
@@ -66,6 +67,43 @@ function NavStatusDot({ statusDot }: { statusDot?: NavStatusDot }) {
|
|
|
66
67
|
);
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
function statusIconClass(tone: NavStatusIcon["tone"]) {
|
|
71
|
+
switch (tone) {
|
|
72
|
+
case "warning":
|
|
73
|
+
return "text-warning";
|
|
74
|
+
case "danger":
|
|
75
|
+
return "text-destructive";
|
|
76
|
+
case "info":
|
|
77
|
+
return "text-primary";
|
|
78
|
+
case "neutral":
|
|
79
|
+
default:
|
|
80
|
+
return "text-sidebar-foreground/60";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function NavStatusIcon({ statusIcon }: { statusIcon?: NavStatusIcon }) {
|
|
85
|
+
if (!statusIcon) return null;
|
|
86
|
+
const Icon = statusIcon.icon;
|
|
87
|
+
// Wrap in a ``<span>`` so the browser tooltip (``title``) and the
|
|
88
|
+
// screen-reader announcement (``aria-label``) attach to a real DOM
|
|
89
|
+
// node — lucide-react's icon types don't accept ``title`` as a
|
|
90
|
+
// prop. Kept small (size-3) so the icon doesn't compete with the
|
|
91
|
+
// entity icon on the left of the row.
|
|
92
|
+
return (
|
|
93
|
+
<span
|
|
94
|
+
role="img"
|
|
95
|
+
aria-label={statusIcon.label}
|
|
96
|
+
title={statusIcon.label}
|
|
97
|
+
className="inline-flex shrink-0"
|
|
98
|
+
>
|
|
99
|
+
<Icon
|
|
100
|
+
aria-hidden="true"
|
|
101
|
+
className={cn("size-3 shrink-0", statusIconClass(statusIcon.tone))}
|
|
102
|
+
/>
|
|
103
|
+
</span>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
69
107
|
// --- Generic drag-and-drop via NavItem.dnd ---
|
|
70
108
|
|
|
71
109
|
/** Shared drag-and-drop props derived from NavItem.dnd (feature-provided). */
|
|
@@ -447,6 +485,7 @@ function NavLeafItem({
|
|
|
447
485
|
{item.badge}
|
|
448
486
|
</span>
|
|
449
487
|
)}
|
|
488
|
+
<NavStatusIcon statusIcon={item.statusIcon} />
|
|
450
489
|
<NavStatusDot statusDot={item.statusDot} />
|
|
451
490
|
</button>
|
|
452
491
|
{/* File-drop hint icon — rendered as an inline flex sibling so it
|
|
@@ -505,6 +544,7 @@ function NavLeafItem({
|
|
|
505
544
|
{item.badge}
|
|
506
545
|
</span>
|
|
507
546
|
)}
|
|
547
|
+
<NavStatusIcon statusIcon={item.statusIcon} />
|
|
508
548
|
<NavStatusDot statusDot={item.statusDot} />
|
|
509
549
|
{isFileDragOver && !isDisabled && (
|
|
510
550
|
<Upload
|
|
@@ -676,6 +716,7 @@ function NestedFolderItem({ item, depth }: { item: NavItem; depth: number }) {
|
|
|
676
716
|
{item.badge}
|
|
677
717
|
</span>
|
|
678
718
|
)}
|
|
719
|
+
<NavStatusIcon statusIcon={item.statusIcon} />
|
|
679
720
|
<NavStatusDot statusDot={item.statusDot} />
|
|
680
721
|
</button>
|
|
681
722
|
{/* File-drop hint icon — inline flex sibling so it sits to the
|
|
@@ -818,6 +859,7 @@ function CollapsibleNavItem({ item }: { item: NavItem }) {
|
|
|
818
859
|
{item.badge}
|
|
819
860
|
</span>
|
|
820
861
|
)}
|
|
862
|
+
<NavStatusIcon statusIcon={item.statusIcon} />
|
|
821
863
|
<NavStatusDot statusDot={item.statusDot} />
|
|
822
864
|
</button>
|
|
823
865
|
{/* File-drop hint icon — inline flex sibling so it sits to the
|
|
@@ -904,6 +946,7 @@ function CollapsibleNavItem({ item }: { item: NavItem }) {
|
|
|
904
946
|
{item.badge}
|
|
905
947
|
</span>
|
|
906
948
|
)}
|
|
949
|
+
<NavStatusIcon statusIcon={item.statusIcon} />
|
|
907
950
|
<NavStatusDot statusDot={item.statusDot} />
|
|
908
951
|
{isFileDragOver && (
|
|
909
952
|
<Upload
|
package/src/lib/nav-config.ts
CHANGED
|
@@ -60,6 +60,17 @@ export interface NavItem {
|
|
|
60
60
|
tone?: "neutral" | "info" | "low-impact" | "medium-impact" | "high-impact" | "pending";
|
|
61
61
|
label?: string;
|
|
62
62
|
};
|
|
63
|
+
/**
|
|
64
|
+
* Optional warning / status icon shown after the label. Use for
|
|
65
|
+
* "something needs attention" annotations (e.g. failed indexing,
|
|
66
|
+
* permission drift) where a coloured dot isn't legible enough.
|
|
67
|
+
* Pass any LucideIcon; the tone drives the colour.
|
|
68
|
+
*/
|
|
69
|
+
statusIcon?: {
|
|
70
|
+
icon: LucideIcon;
|
|
71
|
+
tone?: "neutral" | "info" | "warning" | "danger";
|
|
72
|
+
label?: string;
|
|
73
|
+
};
|
|
63
74
|
/** Page description sent to the agent for context */
|
|
64
75
|
description?: string;
|
|
65
76
|
/** Arbitrary metadata sent to the agent (e.g., workspace IDs, entity types) */
|