@agent-native/toolkit 0.211.0 → 0.212.0-nightly-20261021010049
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/agent-native.eject.json +1 -0
- package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.js +23 -0
- package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.js +23 -0
- package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
- package/dist/app/i18n/catalogs/en-US.d.ts +23 -0
- package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/en-US.js +23 -0
- package/dist/app/i18n/catalogs/en-US.js.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.js +23 -0
- package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.js +23 -0
- package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.js +23 -0
- package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.js +23 -0
- package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.js +23 -0
- package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.js +23 -0
- package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.js +23 -0
- package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.js +23 -0
- package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
- package/dist/app/sharing/OwnershipFilter.d.ts +8 -0
- package/dist/app/sharing/OwnershipFilter.d.ts.map +1 -0
- package/dist/app/sharing/OwnershipFilter.js +20 -0
- package/dist/app/sharing/OwnershipFilter.js.map +1 -0
- package/dist/app/sharing/ShareButton.d.ts +43 -1
- package/dist/app/sharing/ShareButton.d.ts.map +1 -1
- package/dist/app/sharing/ShareButton.js +102 -63
- package/dist/app/sharing/ShareButton.js.map +1 -1
- package/dist/app/sharing/ShareDialog.d.ts +3 -1
- package/dist/app/sharing/ShareDialog.d.ts.map +1 -1
- package/dist/app/sharing/ShareDialog.js +18 -12
- package/dist/app/sharing/ShareDialog.js.map +1 -1
- package/dist/app/sharing/ShareVisibilityConfirmDialog.d.ts +20 -0
- package/dist/app/sharing/ShareVisibilityConfirmDialog.d.ts.map +1 -0
- package/dist/app/sharing/ShareVisibilityConfirmDialog.js +57 -0
- package/dist/app/sharing/ShareVisibilityConfirmDialog.js.map +1 -0
- package/dist/app/sharing/VisibilityBadge.d.ts +8 -1
- package/dist/app/sharing/VisibilityBadge.d.ts.map +1 -1
- package/dist/app/sharing/VisibilityBadge.js +16 -1
- package/dist/app/sharing/VisibilityBadge.js.map +1 -1
- package/dist/app/sharing/index.d.ts +2 -0
- package/dist/app/sharing/index.d.ts.map +1 -1
- package/dist/app/sharing/index.js +2 -0
- package/dist/app/sharing/index.js.map +1 -1
- package/dist/chat-history/ChatHistoryList.d.ts +19 -0
- package/dist/chat-history/ChatHistoryList.d.ts.map +1 -1
- package/dist/chat-history/ChatHistoryList.js +19 -7
- package/dist/chat-history/ChatHistoryList.js.map +1 -1
- package/dist/chat-history/ChatHistoryRail.d.ts +55 -8
- package/dist/chat-history/ChatHistoryRail.d.ts.map +1 -1
- package/dist/chat-history/ChatHistoryRail.js +33 -7
- package/dist/chat-history/ChatHistoryRail.js.map +1 -1
- package/dist/chat-history/index.d.ts +3 -3
- package/dist/chat-history/index.d.ts.map +1 -1
- package/dist/chat-history/index.js +1 -1
- package/dist/chat-history/index.js.map +1 -1
- package/dist/chat-history/useChatHistoryRailController.d.ts +44 -0
- package/dist/chat-history/useChatHistoryRailController.d.ts.map +1 -1
- package/dist/chat-history/useChatHistoryRailController.js +46 -0
- package/dist/chat-history/useChatHistoryRailController.js.map +1 -1
- package/dist/chat-history.css +29 -0
- package/dist/composer/TiptapComposer.d.ts +7 -0
- package/dist/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/composer/TiptapComposer.js +15 -2
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/dist/sharing/OwnershipFilter.d.ts +21 -0
- package/dist/sharing/OwnershipFilter.d.ts.map +1 -0
- package/dist/sharing/OwnershipFilter.js +30 -0
- package/dist/sharing/OwnershipFilter.js.map +1 -0
- package/dist/sharing/VisibilityBadge.d.ts +12 -2
- package/dist/sharing/VisibilityBadge.d.ts.map +1 -1
- package/dist/sharing/VisibilityBadge.js +13 -4
- package/dist/sharing/VisibilityBadge.js.map +1 -1
- package/dist/sharing/index.d.ts +2 -1
- package/dist/sharing/index.d.ts.map +1 -1
- package/dist/sharing/index.js +2 -1
- package/dist/sharing/index.js.map +1 -1
- package/dist/ui/alert-dialog.d.ts +2 -0
- package/dist/ui/alert-dialog.d.ts.map +1 -1
- package/dist/ui/alert-dialog.js +1 -1
- package/dist/ui/alert-dialog.js.map +1 -1
- package/dist/ui/tooltip.d.ts +6 -1
- package/dist/ui/tooltip.d.ts.map +1 -1
- package/dist/ui/tooltip.js +13 -1
- package/dist/ui/tooltip.js.map +1 -1
- package/package.json +4 -4
- package/src/app/i18n/catalogs/ar-SA.ts +26 -0
- package/src/app/i18n/catalogs/de-DE.ts +25 -0
- package/src/app/i18n/catalogs/en-US.ts +25 -0
- package/src/app/i18n/catalogs/es-ES.ts +25 -0
- package/src/app/i18n/catalogs/fr-FR.ts +25 -0
- package/src/app/i18n/catalogs/hi-IN.ts +25 -0
- package/src/app/i18n/catalogs/ja-JP.ts +25 -0
- package/src/app/i18n/catalogs/ko-KR.ts +25 -0
- package/src/app/i18n/catalogs/pt-BR.ts +25 -0
- package/src/app/i18n/catalogs/zh-CN.ts +25 -0
- package/src/app/i18n/catalogs/zh-TW.ts +25 -0
- package/src/app/sharing/OwnershipFilter.tsx +35 -0
- package/src/app/sharing/ShareButton.tsx +384 -259
- package/src/app/sharing/ShareDialog.spec.tsx +50 -0
- package/src/app/sharing/ShareDialog.tsx +51 -15
- package/src/app/sharing/ShareVisibilityConfirmDialog.spec.tsx +138 -0
- package/src/app/sharing/ShareVisibilityConfirmDialog.tsx +124 -0
- package/src/app/sharing/ShareVisibilityKeyboard.spec.tsx +244 -0
- package/src/app/sharing/VisibilityBadge.spec.tsx +65 -0
- package/src/app/sharing/VisibilityBadge.tsx +31 -3
- package/src/app/sharing/index.ts +2 -0
- package/src/app/sharing/share-visibility-mutation-lifecycle.spec.tsx +1107 -0
- package/src/app/sharing/useShareButtonController.spec.tsx +226 -37
- package/src/app/sharing/useShareDialogController.spec.tsx +357 -9
- package/src/chat-history/ChatHistoryList.spec.tsx +157 -0
- package/src/chat-history/ChatHistoryList.tsx +104 -10
- package/src/chat-history/ChatHistoryRail.spec.tsx +50 -1
- package/src/chat-history/ChatHistoryRail.tsx +177 -26
- package/src/chat-history/ChatHistoryRailSections.spec.tsx +410 -0
- package/src/chat-history/index.ts +8 -0
- package/src/chat-history/useChatHistoryRailController.ts +94 -0
- package/src/chat-history/useChatHistoryRailSectionsController.spec.tsx +133 -0
- package/src/chat-history.css +29 -0
- package/src/composer/TiptapComposer.spec.ts +19 -0
- package/src/composer/TiptapComposer.tsx +26 -5
- package/src/sharing/OwnershipFilter.spec.tsx +108 -0
- package/src/sharing/OwnershipFilter.tsx +103 -0
- package/src/sharing/VisibilityBadge.spec.ts +53 -35
- package/src/sharing/VisibilityBadge.tsx +23 -3
- package/src/sharing/index.ts +11 -0
- package/src/ui/alert-dialog.tsx +7 -2
- package/src/ui/tooltip.tsx +22 -1
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
hasConfiguredCloudProvider,
|
|
45
45
|
MODEL_SELECTOR_POPOVER_STYLE,
|
|
46
46
|
mentionItemMatchesQuery,
|
|
47
|
+
modelProviderGroupLabel,
|
|
47
48
|
resolveContextChipBackspaceAction,
|
|
48
49
|
resolveComposerPrimaryAction,
|
|
49
50
|
shouldRenderModelSelector,
|
|
@@ -5043,6 +5044,24 @@ describe("composerModelCostTier", () => {
|
|
|
5043
5044
|
});
|
|
5044
5045
|
});
|
|
5045
5046
|
|
|
5047
|
+
describe("modelProviderGroupLabel", () => {
|
|
5048
|
+
it("drops the Builder.io suffix when the family name is unique", () => {
|
|
5049
|
+
const builder = { engine: "builder", label: "OpenAI · Builder.io" };
|
|
5050
|
+
const claude = { engine: "builder", label: "Claude · Builder.io" };
|
|
5051
|
+
expect(modelProviderGroupLabel(builder, [builder, claude])).toBe("OpenAI");
|
|
5052
|
+
});
|
|
5053
|
+
|
|
5054
|
+
it("keeps the suffix when a direct-key group shares the family name", () => {
|
|
5055
|
+
const builder = { engine: "builder", label: "OpenAI · Builder.io" };
|
|
5056
|
+
const direct = { engine: "ai-sdk:openai", label: "OpenAI" };
|
|
5057
|
+
const groups = [builder, direct];
|
|
5058
|
+
expect(modelProviderGroupLabel(builder, groups)).toBe(
|
|
5059
|
+
"OpenAI · Builder.io",
|
|
5060
|
+
);
|
|
5061
|
+
expect(modelProviderGroupLabel(direct, groups)).toBe("OpenAI");
|
|
5062
|
+
});
|
|
5063
|
+
});
|
|
5064
|
+
|
|
5046
5065
|
describe("voice composer insertion", () => {
|
|
5047
5066
|
it("adds sentence punctuation and a trailing separator to dictated text", () => {
|
|
5048
5067
|
expect(formatVoiceTranscriptForComposer(" First sentence ")).toBe(
|
|
@@ -1599,6 +1599,25 @@ export function isOpenAiModelProviderGroup(group: {
|
|
|
1599
1599
|
);
|
|
1600
1600
|
}
|
|
1601
1601
|
|
|
1602
|
+
const BUILDER_GROUP_SUFFIX = / · Builder\.io$/i;
|
|
1603
|
+
|
|
1604
|
+
// The suffix is the only thing separating Builder.io credits from a group
|
|
1605
|
+
// billed to the user's own key for the same family, so keep it on collision.
|
|
1606
|
+
export function modelProviderGroupLabel(
|
|
1607
|
+
group: { engine: string; label: string },
|
|
1608
|
+
groups: ReadonlyArray<{ engine: string; label: string }>,
|
|
1609
|
+
): string {
|
|
1610
|
+
const short = group.label.replace(BUILDER_GROUP_SUFFIX, "");
|
|
1611
|
+
if (short === group.label) return group.label;
|
|
1612
|
+
const collides = groups.some(
|
|
1613
|
+
(other) =>
|
|
1614
|
+
other !== group &&
|
|
1615
|
+
other.label.replace(BUILDER_GROUP_SUFFIX, "").toLowerCase() ===
|
|
1616
|
+
short.toLowerCase(),
|
|
1617
|
+
);
|
|
1618
|
+
return collides ? group.label : short;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1602
1621
|
const HARNESS_AGENTS_DOCS_URL =
|
|
1603
1622
|
"https://www.agent-native.com/docs/harness-agents";
|
|
1604
1623
|
|
|
@@ -2133,10 +2152,9 @@ function ModelSelector({
|
|
|
2133
2152
|
friendlyModelName(candidate, t)) === selectedModelFriendlyName,
|
|
2134
2153
|
),
|
|
2135
2154
|
);
|
|
2136
|
-
const selectedModelProviderLabel =
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
);
|
|
2155
|
+
const selectedModelProviderLabel =
|
|
2156
|
+
selectedModelProviderGroup &&
|
|
2157
|
+
modelProviderGroupLabel(selectedModelProviderGroup, modelProviderGroups);
|
|
2140
2158
|
const selectedModelDisplayLabel = selectedModelHasDuplicateName
|
|
2141
2159
|
? `${selectedModelFriendlyName} · ${selectedModelProviderLabel}`
|
|
2142
2160
|
: selectedModelFriendlyName;
|
|
@@ -2682,7 +2700,10 @@ function ModelSelector({
|
|
|
2682
2700
|
{showProviderLabels && (
|
|
2683
2701
|
<div className="group flex items-center px-2 py-1">
|
|
2684
2702
|
<span className="shrink-0 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
2685
|
-
{
|
|
2703
|
+
{modelProviderGroupLabel(
|
|
2704
|
+
group,
|
|
2705
|
+
visibleProviderGroups,
|
|
2706
|
+
)}
|
|
2686
2707
|
</span>
|
|
2687
2708
|
{!group.configured && statusLabel && (
|
|
2688
2709
|
<button
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
filterByOwnership,
|
|
9
|
+
OwnershipFilter,
|
|
10
|
+
type OwnershipFilterValue,
|
|
11
|
+
} from "./OwnershipFilter.js";
|
|
12
|
+
|
|
13
|
+
const labels = { all: "Todos", mine: "Míos", shared: "Compartidos" };
|
|
14
|
+
|
|
15
|
+
describe("filterByOwnership", () => {
|
|
16
|
+
const rows = [
|
|
17
|
+
{ id: "a", mine: true },
|
|
18
|
+
{ id: "b", mine: false },
|
|
19
|
+
{ id: "c", mine: true },
|
|
20
|
+
];
|
|
21
|
+
const isMine = (row: { mine: boolean }) => row.mine;
|
|
22
|
+
|
|
23
|
+
it("keeps everything for all, without aliasing the input", () => {
|
|
24
|
+
const result = filterByOwnership(rows, "all", isMine);
|
|
25
|
+
expect(result).toEqual(rows);
|
|
26
|
+
expect(result).not.toBe(rows);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("splits owned and not-owned rows between mine and shared", () => {
|
|
30
|
+
expect(filterByOwnership(rows, "mine", isMine).map((r) => r.id)).toEqual([
|
|
31
|
+
"a",
|
|
32
|
+
"c",
|
|
33
|
+
]);
|
|
34
|
+
expect(filterByOwnership(rows, "shared", isMine).map((r) => r.id)).toEqual([
|
|
35
|
+
"b",
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("OwnershipFilter", () => {
|
|
41
|
+
let container: HTMLDivElement;
|
|
42
|
+
let root: Root;
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
46
|
+
container = document.createElement("div");
|
|
47
|
+
document.body.appendChild(container);
|
|
48
|
+
root = createRoot(container);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
act(() => root.unmount());
|
|
53
|
+
container.remove();
|
|
54
|
+
vi.unstubAllGlobals();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function renderFilter(
|
|
58
|
+
value: OwnershipFilterValue,
|
|
59
|
+
onChange: (value: OwnershipFilterValue) => void = () => {},
|
|
60
|
+
) {
|
|
61
|
+
act(() =>
|
|
62
|
+
root.render(
|
|
63
|
+
<OwnershipFilter value={value} onChange={onChange} labels={labels} />,
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
return container.querySelector<HTMLButtonElement>("button")!;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
it("names the trigger after the selected option and reserves room for the widest", () => {
|
|
70
|
+
const trigger = renderFilter("mine");
|
|
71
|
+
expect(trigger.getAttribute("aria-label")).toBe("Míos");
|
|
72
|
+
const hidden = Array.from(
|
|
73
|
+
trigger.querySelectorAll('[aria-hidden="true"].invisible'),
|
|
74
|
+
).map((node) => node.textContent);
|
|
75
|
+
expect(hidden).toEqual(["Todos", "Míos", "Compartidos"]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("reports the chosen option from the menu and ignores unknown values", async () => {
|
|
79
|
+
const onChange = vi.fn();
|
|
80
|
+
const trigger = renderFilter("all", onChange);
|
|
81
|
+
await act(async () => {
|
|
82
|
+
trigger.dispatchEvent(
|
|
83
|
+
new PointerEvent("pointerdown", {
|
|
84
|
+
bubbles: true,
|
|
85
|
+
button: 0,
|
|
86
|
+
pointerType: "mouse",
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
await Promise.resolve();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const items = Array.from(
|
|
93
|
+
document.body.querySelectorAll<HTMLElement>('[role="menuitemradio"]'),
|
|
94
|
+
);
|
|
95
|
+
expect(items.map((item) => item.textContent)).toEqual([
|
|
96
|
+
"Todos",
|
|
97
|
+
"Míos",
|
|
98
|
+
"Compartidos",
|
|
99
|
+
]);
|
|
100
|
+
expect(items[0]?.getAttribute("aria-checked")).toBe("true");
|
|
101
|
+
|
|
102
|
+
await act(async () => {
|
|
103
|
+
items[2]?.click();
|
|
104
|
+
await Promise.resolve();
|
|
105
|
+
});
|
|
106
|
+
expect(onChange).toHaveBeenCalledExactlyOnceWith("shared");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { IconChevronDown } from "@tabler/icons-react";
|
|
2
|
+
|
|
3
|
+
import { Button } from "../ui/button.js";
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuRadioGroup,
|
|
8
|
+
DropdownMenuRadioItem,
|
|
9
|
+
DropdownMenuTrigger,
|
|
10
|
+
} from "../ui/dropdown-menu.js";
|
|
11
|
+
import { cn } from "../utils.js";
|
|
12
|
+
|
|
13
|
+
export const OWNERSHIP_FILTER_VALUES = ["all", "mine", "shared"] as const;
|
|
14
|
+
|
|
15
|
+
export type OwnershipFilterValue = (typeof OWNERSHIP_FILTER_VALUES)[number];
|
|
16
|
+
|
|
17
|
+
/** Translated by the app; the toolkit ships no copy for the three options. */
|
|
18
|
+
export type OwnershipFilterLabels = Record<OwnershipFilterValue, string>;
|
|
19
|
+
|
|
20
|
+
export interface OwnershipFilterProps {
|
|
21
|
+
value: OwnershipFilterValue;
|
|
22
|
+
onChange: (value: OwnershipFilterValue) => void;
|
|
23
|
+
labels: OwnershipFilterLabels;
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isOwnershipFilterValue(value: string): value is OwnershipFilterValue {
|
|
28
|
+
return (OWNERSHIP_FILTER_VALUES as readonly string[]).includes(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pure and app-controlled: it renders the choice and reports it. Whether the
|
|
33
|
+
* app narrows rows client-side or refetches with the value is the app's call.
|
|
34
|
+
*/
|
|
35
|
+
export function OwnershipFilter({
|
|
36
|
+
value,
|
|
37
|
+
onChange,
|
|
38
|
+
labels,
|
|
39
|
+
className,
|
|
40
|
+
}: OwnershipFilterProps) {
|
|
41
|
+
const label = labels[value];
|
|
42
|
+
return (
|
|
43
|
+
<DropdownMenu>
|
|
44
|
+
<DropdownMenuTrigger asChild>
|
|
45
|
+
<Button
|
|
46
|
+
type="button"
|
|
47
|
+
variant="outline"
|
|
48
|
+
size="sm"
|
|
49
|
+
aria-label={label}
|
|
50
|
+
className={cn("h-9 shrink-0 gap-2", className)}
|
|
51
|
+
>
|
|
52
|
+
{/* Every option is rendered invisibly so the trigger keeps one width. */}
|
|
53
|
+
<span className="grid">
|
|
54
|
+
{OWNERSHIP_FILTER_VALUES.map((option) => (
|
|
55
|
+
<span
|
|
56
|
+
key={option}
|
|
57
|
+
aria-hidden="true"
|
|
58
|
+
className="invisible col-start-1 row-start-1 whitespace-nowrap"
|
|
59
|
+
>
|
|
60
|
+
{labels[option]}
|
|
61
|
+
</span>
|
|
62
|
+
))}
|
|
63
|
+
<span className="col-start-1 row-start-1 whitespace-nowrap">
|
|
64
|
+
{label}
|
|
65
|
+
</span>
|
|
66
|
+
</span>
|
|
67
|
+
<IconChevronDown className="size-4" aria-hidden="true" />
|
|
68
|
+
</Button>
|
|
69
|
+
</DropdownMenuTrigger>
|
|
70
|
+
<DropdownMenuContent align="start">
|
|
71
|
+
<DropdownMenuRadioGroup
|
|
72
|
+
value={value}
|
|
73
|
+
onValueChange={(next) => {
|
|
74
|
+
if (isOwnershipFilterValue(next)) onChange(next);
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
{OWNERSHIP_FILTER_VALUES.map((option) => (
|
|
78
|
+
<DropdownMenuRadioItem
|
|
79
|
+
key={option}
|
|
80
|
+
value={option}
|
|
81
|
+
indicator="check"
|
|
82
|
+
>
|
|
83
|
+
{labels[option]}
|
|
84
|
+
</DropdownMenuRadioItem>
|
|
85
|
+
))}
|
|
86
|
+
</DropdownMenuRadioGroup>
|
|
87
|
+
</DropdownMenuContent>
|
|
88
|
+
</DropdownMenu>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Client-side narrowing for apps that already hold every row. Apps that page
|
|
94
|
+
* from the server should send the filter value instead and skip this.
|
|
95
|
+
*/
|
|
96
|
+
export function filterByOwnership<T>(
|
|
97
|
+
items: readonly T[],
|
|
98
|
+
value: OwnershipFilterValue,
|
|
99
|
+
isMine: (item: T) => boolean,
|
|
100
|
+
): T[] {
|
|
101
|
+
if (value === "all") return [...items];
|
|
102
|
+
return items.filter((item) => isMine(item) === (value === "mine"));
|
|
103
|
+
}
|
|
@@ -17,72 +17,90 @@ interface SpanElement extends ReactElement {
|
|
|
17
17
|
props: {
|
|
18
18
|
className?: string;
|
|
19
19
|
style: SpanStyle;
|
|
20
|
-
|
|
20
|
+
"data-visibility": string;
|
|
21
|
+
children: [ReactElement<{ size: number; "aria-hidden"?: string }>, string];
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
const labels = { private: "Privé", org: "Équipe", public: "Public·fr" };
|
|
26
|
+
|
|
24
27
|
function renderBadge(...args: Parameters<typeof VisibilityBadge>): SpanElement {
|
|
25
28
|
return VisibilityBadge(...args) as unknown as SpanElement;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
describe("VisibilityBadge", () => {
|
|
29
|
-
it("renders the
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
it("renders the icon and the supplied label for each visibility", () => {
|
|
33
|
+
const cases = [
|
|
34
|
+
["public", IconWorld, "Public·fr"],
|
|
35
|
+
["org", IconUsersGroup, "Équipe"],
|
|
36
|
+
["private", IconLock, "Privé"],
|
|
37
|
+
] as const;
|
|
38
|
+
for (const [visibility, expectedIcon, expectedLabel] of cases) {
|
|
39
|
+
const el = renderBadge({ visibility, labels });
|
|
40
|
+
const [icon, label] = el.props.children;
|
|
41
|
+
expect(el.type).toBe("span");
|
|
42
|
+
expect(icon.type).toBe(expectedIcon);
|
|
43
|
+
expect(label).toBe(expectedLabel);
|
|
44
|
+
expect(el.props["data-visibility"]).toBe(visibility);
|
|
45
|
+
}
|
|
35
46
|
});
|
|
36
47
|
|
|
37
|
-
it("
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
expect(
|
|
41
|
-
expect(
|
|
48
|
+
it("keeps the legacy English text for consumers that pass no labels", () => {
|
|
49
|
+
const text = (visibility: "private" | "org" | "public") =>
|
|
50
|
+
renderBadge({ visibility }).props.children[1];
|
|
51
|
+
expect(text("public")).toBe("Public");
|
|
52
|
+
expect(text("org")).toBe("Org");
|
|
53
|
+
expect(text("private")).toBe("Private");
|
|
42
54
|
});
|
|
43
55
|
|
|
44
|
-
it("
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
it("lets labels override one visibility while the others keep the default", () => {
|
|
57
|
+
expect(
|
|
58
|
+
renderBadge({ visibility: "org", labels: { org: "Team" } }).props
|
|
59
|
+
.children[1],
|
|
60
|
+
).toBe("Team");
|
|
61
|
+
expect(
|
|
62
|
+
renderBadge({ visibility: "public", labels: { org: "Team" } }).props
|
|
63
|
+
.children[1],
|
|
64
|
+
).toBe("Public");
|
|
49
65
|
});
|
|
50
66
|
|
|
51
|
-
it("
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
expect(icon.type).toBe(IconLock);
|
|
55
|
-
expect(label).toBe("Private");
|
|
67
|
+
it("hides the icon from assistive tech because the text names it", () => {
|
|
68
|
+
const [icon] = renderBadge({ visibility: "org" }).props.children;
|
|
69
|
+
expect(icon.props["aria-hidden"]).toBe("true");
|
|
56
70
|
});
|
|
57
71
|
|
|
58
|
-
it("falls back to private when visibility is undefined", () => {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
72
|
+
it("falls back to private when visibility is null or undefined", () => {
|
|
73
|
+
for (const visibility of [null, undefined]) {
|
|
74
|
+
const el = renderBadge({ visibility, labels });
|
|
75
|
+
const [icon, label] = el.props.children;
|
|
76
|
+
expect(icon.type).toBe(IconLock);
|
|
77
|
+
expect(label).toBe("Privé");
|
|
78
|
+
}
|
|
63
79
|
});
|
|
64
80
|
|
|
65
81
|
it("applies a custom size to both the span font size and the icon size", () => {
|
|
66
|
-
const el = renderBadge({ visibility: "public", size: 20 });
|
|
82
|
+
const el = renderBadge({ visibility: "public", size: 20, labels });
|
|
67
83
|
expect(el.props.style.fontSize).toBe(20);
|
|
68
|
-
|
|
69
|
-
expect(icon.props.size).toBe(22);
|
|
84
|
+
expect(el.props.children[0].props.size).toBe(22);
|
|
70
85
|
});
|
|
71
86
|
|
|
72
87
|
it("defaults size to 12 for the span and 14 for the icon when omitted", () => {
|
|
73
|
-
const el = renderBadge({ visibility: "public" });
|
|
88
|
+
const el = renderBadge({ visibility: "public", labels });
|
|
74
89
|
expect(el.props.style.fontSize).toBe(12);
|
|
75
|
-
|
|
76
|
-
expect(icon.props.size).toBe(14);
|
|
90
|
+
expect(el.props.children[0].props.size).toBe(14);
|
|
77
91
|
});
|
|
78
92
|
|
|
79
93
|
it("passes className through to the span", () => {
|
|
80
|
-
const el = renderBadge({
|
|
94
|
+
const el = renderBadge({
|
|
95
|
+
visibility: "public",
|
|
96
|
+
labels,
|
|
97
|
+
className: "my-badge",
|
|
98
|
+
});
|
|
81
99
|
expect(el.props.className).toBe("my-badge");
|
|
82
100
|
});
|
|
83
101
|
|
|
84
102
|
it("always uses the muted-foreground color token", () => {
|
|
85
|
-
const el = renderBadge({ visibility: "org" });
|
|
103
|
+
const el = renderBadge({ visibility: "org", labels });
|
|
86
104
|
expect(el.props.style.color).toBe("hsl(var(--muted-foreground))");
|
|
87
105
|
});
|
|
88
106
|
});
|
|
@@ -1,23 +1,43 @@
|
|
|
1
1
|
import { IconLock, IconUsersGroup, IconWorld } from "@tabler/icons-react";
|
|
2
2
|
|
|
3
|
+
export type VisibilityBadgeValue = "private" | "org" | "public";
|
|
4
|
+
|
|
5
|
+
/** Translated by the app; the pure badge ships no copy of its own. */
|
|
6
|
+
export type VisibilityBadgeLabels = Record<VisibilityBadgeValue, string>;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Legacy English copy for standalone consumers that pass no labels. Apps with
|
|
10
|
+
* a Core catalog should use the localized badge in `toolkit/app/sharing`.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_ENGLISH_VISIBILITY_BADGE_LABELS: VisibilityBadgeLabels = {
|
|
13
|
+
private: "Private",
|
|
14
|
+
org: "Org",
|
|
15
|
+
public: "Public",
|
|
16
|
+
};
|
|
17
|
+
|
|
3
18
|
export interface VisibilityBadgeProps {
|
|
4
|
-
visibility:
|
|
19
|
+
visibility: VisibilityBadgeValue | null | undefined;
|
|
20
|
+
/** Translated copy; omitted labels fall back to the English defaults. */
|
|
21
|
+
labels?: Partial<VisibilityBadgeLabels>;
|
|
5
22
|
size?: number;
|
|
6
23
|
className?: string;
|
|
7
24
|
}
|
|
8
25
|
|
|
9
26
|
export function VisibilityBadge({
|
|
10
27
|
visibility,
|
|
28
|
+
labels: labelOverrides,
|
|
11
29
|
size = 12,
|
|
12
30
|
className,
|
|
13
31
|
}: VisibilityBadgeProps) {
|
|
14
32
|
const v = visibility ?? "private";
|
|
33
|
+
const label =
|
|
34
|
+
labelOverrides?.[v] ?? DEFAULT_ENGLISH_VISIBILITY_BADGE_LABELS[v];
|
|
15
35
|
const Icon =
|
|
16
36
|
v === "public" ? IconWorld : v === "org" ? IconUsersGroup : IconLock;
|
|
17
|
-
const label = v === "public" ? "Public" : v === "org" ? "Org" : "Private";
|
|
18
37
|
return (
|
|
19
38
|
<span
|
|
20
39
|
className={className}
|
|
40
|
+
data-visibility={v}
|
|
21
41
|
style={{
|
|
22
42
|
display: "inline-flex",
|
|
23
43
|
alignItems: "center",
|
|
@@ -26,7 +46,7 @@ export function VisibilityBadge({
|
|
|
26
46
|
color: "hsl(var(--muted-foreground))",
|
|
27
47
|
}}
|
|
28
48
|
>
|
|
29
|
-
<Icon size={size + 2} />
|
|
49
|
+
<Icon size={size + 2} aria-hidden="true" />
|
|
30
50
|
{label}
|
|
31
51
|
</span>
|
|
32
52
|
);
|
package/src/sharing/index.ts
CHANGED
|
@@ -19,6 +19,14 @@ export {
|
|
|
19
19
|
ShareTabs,
|
|
20
20
|
type ShareTabsProps,
|
|
21
21
|
} from "./ShareControls.js";
|
|
22
|
+
export {
|
|
23
|
+
filterByOwnership,
|
|
24
|
+
OWNERSHIP_FILTER_VALUES,
|
|
25
|
+
OwnershipFilter,
|
|
26
|
+
type OwnershipFilterLabels,
|
|
27
|
+
type OwnershipFilterProps,
|
|
28
|
+
type OwnershipFilterValue,
|
|
29
|
+
} from "./OwnershipFilter.js";
|
|
22
30
|
export {
|
|
23
31
|
ShareAgentsSection,
|
|
24
32
|
type ShareAgentsSectionProps,
|
|
@@ -32,6 +40,9 @@ export {
|
|
|
32
40
|
type ShareTriggerProps,
|
|
33
41
|
} from "./SharePrimitives.js";
|
|
34
42
|
export {
|
|
43
|
+
DEFAULT_ENGLISH_VISIBILITY_BADGE_LABELS,
|
|
35
44
|
VisibilityBadge,
|
|
45
|
+
type VisibilityBadgeLabels,
|
|
36
46
|
type VisibilityBadgeProps,
|
|
47
|
+
type VisibilityBadgeValue,
|
|
37
48
|
} from "./VisibilityBadge.js";
|
package/src/ui/alert-dialog.tsx
CHANGED
|
@@ -30,13 +30,18 @@ AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
|
|
|
30
30
|
const AlertDialogContent = React.forwardRef<
|
|
31
31
|
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
|
32
32
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> & {
|
|
33
|
+
/** Raise the overlay with the content when opening above other layers. */
|
|
34
|
+
overlayStyle?: React.CSSProperties;
|
|
33
35
|
overlayProps?: React.ComponentPropsWithoutRef<
|
|
34
36
|
typeof AlertDialogPrimitive.Overlay
|
|
35
37
|
> & { [dataAttribute: `data-${string}`]: string | undefined };
|
|
36
38
|
}
|
|
37
|
-
>(({ className, style, overlayProps, ...props }, ref) => (
|
|
39
|
+
>(({ className, style, overlayStyle, overlayProps, ...props }, ref) => (
|
|
38
40
|
<AlertDialogPortal>
|
|
39
|
-
<AlertDialogOverlay
|
|
41
|
+
<AlertDialogOverlay
|
|
42
|
+
{...overlayProps}
|
|
43
|
+
style={{ ...overlayProps?.style, ...overlayStyle }}
|
|
44
|
+
/>
|
|
40
45
|
<AlertDialogPrimitive.Content
|
|
41
46
|
ref={ref}
|
|
42
47
|
className={cn(
|
package/src/ui/tooltip.tsx
CHANGED
|
@@ -7,7 +7,28 @@ const TooltipProvider = TooltipPrimitive.Provider;
|
|
|
7
7
|
|
|
8
8
|
const Tooltip = TooltipPrimitive.Root;
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Opens on keyboard focus, but not when a closing menu, popover or dialog
|
|
12
|
+
* hands focus back to the trigger after a click: Radix opens on any focus,
|
|
13
|
+
* so the tooltip would pop up after every such close.
|
|
14
|
+
*/
|
|
15
|
+
const TooltipTrigger = React.forwardRef<
|
|
16
|
+
React.ElementRef<typeof TooltipPrimitive.Trigger>,
|
|
17
|
+
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Trigger>
|
|
18
|
+
>(({ onFocus, ...props }, ref) => (
|
|
19
|
+
<TooltipPrimitive.Trigger
|
|
20
|
+
ref={ref}
|
|
21
|
+
onFocus={(event) => {
|
|
22
|
+
onFocus?.(event);
|
|
23
|
+
// Radix skips its own open-on-focus for a default-prevented event.
|
|
24
|
+
if (!event.currentTarget.matches(":focus-visible")) {
|
|
25
|
+
event.preventDefault();
|
|
26
|
+
}
|
|
27
|
+
}}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
));
|
|
31
|
+
TooltipTrigger.displayName = TooltipPrimitive.Trigger.displayName;
|
|
11
32
|
|
|
12
33
|
function normalizeTooltipText(text: string): string {
|
|
13
34
|
const decoded = text.replace(/\\u([0-9a-fA-F]{4})/g, (_match, hex) =>
|