@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.
Files changed (143) hide show
  1. package/agent-native.eject.json +1 -0
  2. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  3. package/dist/app/i18n/catalogs/ar-SA.js +23 -0
  4. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  5. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  6. package/dist/app/i18n/catalogs/de-DE.js +23 -0
  7. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  8. package/dist/app/i18n/catalogs/en-US.d.ts +23 -0
  9. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  10. package/dist/app/i18n/catalogs/en-US.js +23 -0
  11. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  12. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  13. package/dist/app/i18n/catalogs/es-ES.js +23 -0
  14. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  15. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  16. package/dist/app/i18n/catalogs/fr-FR.js +23 -0
  17. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  18. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  19. package/dist/app/i18n/catalogs/hi-IN.js +23 -0
  20. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  21. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  22. package/dist/app/i18n/catalogs/ja-JP.js +23 -0
  23. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  24. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  25. package/dist/app/i18n/catalogs/ko-KR.js +23 -0
  26. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  27. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  28. package/dist/app/i18n/catalogs/pt-BR.js +23 -0
  29. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  30. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  31. package/dist/app/i18n/catalogs/zh-CN.js +23 -0
  32. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  33. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  34. package/dist/app/i18n/catalogs/zh-TW.js +23 -0
  35. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  36. package/dist/app/sharing/OwnershipFilter.d.ts +8 -0
  37. package/dist/app/sharing/OwnershipFilter.d.ts.map +1 -0
  38. package/dist/app/sharing/OwnershipFilter.js +20 -0
  39. package/dist/app/sharing/OwnershipFilter.js.map +1 -0
  40. package/dist/app/sharing/ShareButton.d.ts +43 -1
  41. package/dist/app/sharing/ShareButton.d.ts.map +1 -1
  42. package/dist/app/sharing/ShareButton.js +102 -63
  43. package/dist/app/sharing/ShareButton.js.map +1 -1
  44. package/dist/app/sharing/ShareDialog.d.ts +3 -1
  45. package/dist/app/sharing/ShareDialog.d.ts.map +1 -1
  46. package/dist/app/sharing/ShareDialog.js +18 -12
  47. package/dist/app/sharing/ShareDialog.js.map +1 -1
  48. package/dist/app/sharing/ShareVisibilityConfirmDialog.d.ts +20 -0
  49. package/dist/app/sharing/ShareVisibilityConfirmDialog.d.ts.map +1 -0
  50. package/dist/app/sharing/ShareVisibilityConfirmDialog.js +57 -0
  51. package/dist/app/sharing/ShareVisibilityConfirmDialog.js.map +1 -0
  52. package/dist/app/sharing/VisibilityBadge.d.ts +8 -1
  53. package/dist/app/sharing/VisibilityBadge.d.ts.map +1 -1
  54. package/dist/app/sharing/VisibilityBadge.js +16 -1
  55. package/dist/app/sharing/VisibilityBadge.js.map +1 -1
  56. package/dist/app/sharing/index.d.ts +2 -0
  57. package/dist/app/sharing/index.d.ts.map +1 -1
  58. package/dist/app/sharing/index.js +2 -0
  59. package/dist/app/sharing/index.js.map +1 -1
  60. package/dist/chat-history/ChatHistoryList.d.ts +19 -0
  61. package/dist/chat-history/ChatHistoryList.d.ts.map +1 -1
  62. package/dist/chat-history/ChatHistoryList.js +19 -7
  63. package/dist/chat-history/ChatHistoryList.js.map +1 -1
  64. package/dist/chat-history/ChatHistoryRail.d.ts +55 -8
  65. package/dist/chat-history/ChatHistoryRail.d.ts.map +1 -1
  66. package/dist/chat-history/ChatHistoryRail.js +33 -7
  67. package/dist/chat-history/ChatHistoryRail.js.map +1 -1
  68. package/dist/chat-history/index.d.ts +3 -3
  69. package/dist/chat-history/index.d.ts.map +1 -1
  70. package/dist/chat-history/index.js +1 -1
  71. package/dist/chat-history/index.js.map +1 -1
  72. package/dist/chat-history/useChatHistoryRailController.d.ts +44 -0
  73. package/dist/chat-history/useChatHistoryRailController.d.ts.map +1 -1
  74. package/dist/chat-history/useChatHistoryRailController.js +46 -0
  75. package/dist/chat-history/useChatHistoryRailController.js.map +1 -1
  76. package/dist/chat-history.css +29 -0
  77. package/dist/composer/TiptapComposer.d.ts +7 -0
  78. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  79. package/dist/composer/TiptapComposer.js +15 -2
  80. package/dist/composer/TiptapComposer.js.map +1 -1
  81. package/dist/sharing/OwnershipFilter.d.ts +21 -0
  82. package/dist/sharing/OwnershipFilter.d.ts.map +1 -0
  83. package/dist/sharing/OwnershipFilter.js +30 -0
  84. package/dist/sharing/OwnershipFilter.js.map +1 -0
  85. package/dist/sharing/VisibilityBadge.d.ts +12 -2
  86. package/dist/sharing/VisibilityBadge.d.ts.map +1 -1
  87. package/dist/sharing/VisibilityBadge.js +13 -4
  88. package/dist/sharing/VisibilityBadge.js.map +1 -1
  89. package/dist/sharing/index.d.ts +2 -1
  90. package/dist/sharing/index.d.ts.map +1 -1
  91. package/dist/sharing/index.js +2 -1
  92. package/dist/sharing/index.js.map +1 -1
  93. package/dist/ui/alert-dialog.d.ts +2 -0
  94. package/dist/ui/alert-dialog.d.ts.map +1 -1
  95. package/dist/ui/alert-dialog.js +1 -1
  96. package/dist/ui/alert-dialog.js.map +1 -1
  97. package/dist/ui/tooltip.d.ts +6 -1
  98. package/dist/ui/tooltip.d.ts.map +1 -1
  99. package/dist/ui/tooltip.js +13 -1
  100. package/dist/ui/tooltip.js.map +1 -1
  101. package/package.json +4 -4
  102. package/src/app/i18n/catalogs/ar-SA.ts +26 -0
  103. package/src/app/i18n/catalogs/de-DE.ts +25 -0
  104. package/src/app/i18n/catalogs/en-US.ts +25 -0
  105. package/src/app/i18n/catalogs/es-ES.ts +25 -0
  106. package/src/app/i18n/catalogs/fr-FR.ts +25 -0
  107. package/src/app/i18n/catalogs/hi-IN.ts +25 -0
  108. package/src/app/i18n/catalogs/ja-JP.ts +25 -0
  109. package/src/app/i18n/catalogs/ko-KR.ts +25 -0
  110. package/src/app/i18n/catalogs/pt-BR.ts +25 -0
  111. package/src/app/i18n/catalogs/zh-CN.ts +25 -0
  112. package/src/app/i18n/catalogs/zh-TW.ts +25 -0
  113. package/src/app/sharing/OwnershipFilter.tsx +35 -0
  114. package/src/app/sharing/ShareButton.tsx +384 -259
  115. package/src/app/sharing/ShareDialog.spec.tsx +50 -0
  116. package/src/app/sharing/ShareDialog.tsx +51 -15
  117. package/src/app/sharing/ShareVisibilityConfirmDialog.spec.tsx +138 -0
  118. package/src/app/sharing/ShareVisibilityConfirmDialog.tsx +124 -0
  119. package/src/app/sharing/ShareVisibilityKeyboard.spec.tsx +244 -0
  120. package/src/app/sharing/VisibilityBadge.spec.tsx +65 -0
  121. package/src/app/sharing/VisibilityBadge.tsx +31 -3
  122. package/src/app/sharing/index.ts +2 -0
  123. package/src/app/sharing/share-visibility-mutation-lifecycle.spec.tsx +1107 -0
  124. package/src/app/sharing/useShareButtonController.spec.tsx +226 -37
  125. package/src/app/sharing/useShareDialogController.spec.tsx +357 -9
  126. package/src/chat-history/ChatHistoryList.spec.tsx +157 -0
  127. package/src/chat-history/ChatHistoryList.tsx +104 -10
  128. package/src/chat-history/ChatHistoryRail.spec.tsx +50 -1
  129. package/src/chat-history/ChatHistoryRail.tsx +177 -26
  130. package/src/chat-history/ChatHistoryRailSections.spec.tsx +410 -0
  131. package/src/chat-history/index.ts +8 -0
  132. package/src/chat-history/useChatHistoryRailController.ts +94 -0
  133. package/src/chat-history/useChatHistoryRailSectionsController.spec.tsx +133 -0
  134. package/src/chat-history.css +29 -0
  135. package/src/composer/TiptapComposer.spec.ts +19 -0
  136. package/src/composer/TiptapComposer.tsx +26 -5
  137. package/src/sharing/OwnershipFilter.spec.tsx +108 -0
  138. package/src/sharing/OwnershipFilter.tsx +103 -0
  139. package/src/sharing/VisibilityBadge.spec.ts +53 -35
  140. package/src/sharing/VisibilityBadge.tsx +23 -3
  141. package/src/sharing/index.ts +11 -0
  142. package/src/ui/alert-dialog.tsx +7 -2
  143. 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 = selectedModelProviderGroup?.label.replace(
2137
- / · Builder\.io$/i,
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
- {group.label.replace(/ · Builder\.io$/i, "")}
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
- children: [ReactElement<{ size: number }>, string];
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 public icon and label", () => {
30
- const el = renderBadge({ visibility: "public" });
31
- expect(el.type).toBe("span");
32
- const [icon, label] = el.props.children;
33
- expect(icon.type).toBe(IconWorld);
34
- expect(label).toBe("Public");
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("renders the org icon and label", () => {
38
- const el = renderBadge({ visibility: "org" });
39
- const [icon, label] = el.props.children;
40
- expect(icon.type).toBe(IconUsersGroup);
41
- expect(label).toBe("Org");
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("renders the private icon and label", () => {
45
- const el = renderBadge({ visibility: "private" });
46
- const [icon, label] = el.props.children;
47
- expect(icon.type).toBe(IconLock);
48
- expect(label).toBe("Private");
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("falls back to private when visibility is null", () => {
52
- const el = renderBadge({ visibility: null });
53
- const [icon, label] = el.props.children;
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 el = renderBadge({ visibility: undefined });
60
- const [icon, label] = el.props.children;
61
- expect(icon.type).toBe(IconLock);
62
- expect(label).toBe("Private");
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
- const [icon] = el.props.children;
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
- const [icon] = el.props.children;
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({ visibility: "public", className: "my-badge" });
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: "private" | "org" | "public" | null | undefined;
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
  );
@@ -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";
@@ -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 {...overlayProps} />
41
+ <AlertDialogOverlay
42
+ {...overlayProps}
43
+ style={{ ...overlayProps?.style, ...overlayStyle }}
44
+ />
40
45
  <AlertDialogPrimitive.Content
41
46
  ref={ref}
42
47
  className={cn(
@@ -7,7 +7,28 @@ const TooltipProvider = TooltipPrimitive.Provider;
7
7
 
8
8
  const Tooltip = TooltipPrimitive.Root;
9
9
 
10
- const TooltipTrigger = TooltipPrimitive.Trigger;
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) =>