@elabs-ai/components-ai 4.0.0 → 4.2.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.
Files changed (180) hide show
  1. package/README.md +62 -12
  2. package/dist/{_audio-player-media-chrome-KA5DY54G.js → _audio-player-media-chrome-T3XVXWRZ.js} +8 -4
  3. package/dist/_audio-player-media-chrome-T3XVXWRZ.js.map +1 -0
  4. package/dist/{_flow-boundary-D63PJ65S.js → _flow-boundary-SHNWLQG5.js} +32 -43
  5. package/dist/_flow-boundary-SHNWLQG5.js.map +1 -0
  6. package/dist/{_persona-rive-RFR2EUWP.js → _persona-rive-JEG44YHX.js} +9 -5
  7. package/dist/_persona-rive-JEG44YHX.js.map +1 -0
  8. package/dist/index.d.ts +1152 -218
  9. package/dist/index.js +5601 -3241
  10. package/dist/index.js.map +1 -1
  11. package/package.json +30 -15
  12. package/src/__contract__/audio-visualizer.contract.test.tsx +49 -0
  13. package/src/__contract__/chat-shell.contract.test.tsx +49 -0
  14. package/src/__contract__/grouped-parts.contract.test.tsx +49 -0
  15. package/src/__contract__/image.contract.test.tsx +49 -0
  16. package/src/__contract__/markdown-view.contract.test.tsx +49 -0
  17. package/src/__contract__/message-feedback.contract.test.tsx +49 -0
  18. package/src/__contract__/message-form.contract.test.tsx +49 -0
  19. package/src/__contract__/message-table.contract.test.tsx +49 -0
  20. package/src/__contract__/model-provider-logo.contract.test.tsx +49 -0
  21. package/src/__contract__/persona.contract.test.tsx +49 -0
  22. package/src/__contract__/prompt-input-effort.contract.test.tsx +49 -0
  23. package/src/__contract__/prompt-input-mode.contract.test.tsx +49 -0
  24. package/src/_audio-player-media-chrome.tsx +102 -15
  25. package/src/_chat-shell-rail.tsx +2 -2
  26. package/src/_flow-boundary.tsx +68 -49
  27. package/src/_lazy-boundary-conformance.ts +38 -0
  28. package/src/_lazy-cjk.test.ts +43 -0
  29. package/src/_lazy-cjk.ts +73 -0
  30. package/src/_lazy-engine-boundary.tsx +61 -0
  31. package/src/_lazy-math.test.ts +63 -0
  32. package/src/_lazy-math.ts +90 -0
  33. package/src/_lazy-mermaid-absent.test.ts +53 -0
  34. package/src/_lazy-mermaid.test.ts +15 -0
  35. package/src/_lazy-mermaid.ts +24 -1
  36. package/src/_mermaid-error-panel.test.tsx +50 -0
  37. package/src/_mermaid-error-panel.tsx +66 -0
  38. package/src/_persona-rive.tsx +62 -10
  39. package/src/_streamdown-i18n.ts +94 -22
  40. package/src/_streamdown-safety.ts +170 -0
  41. package/src/_theme-scope-store.test.ts +83 -0
  42. package/src/_theme-scope-store.ts +103 -0
  43. package/src/agent-event.stories.tsx +97 -0
  44. package/src/agent-event.test.tsx +145 -0
  45. package/src/agent-event.tsx +187 -0
  46. package/src/agent-timeline.stories.tsx +9 -1
  47. package/src/agent.test.tsx +47 -0
  48. package/src/agent.tsx +43 -24
  49. package/src/agentic-workspace.stories.tsx +27 -20
  50. package/src/artifact.tsx +17 -14
  51. package/src/asset-preview.test.tsx +40 -0
  52. package/src/asset-preview.tsx +83 -12
  53. package/src/attachments.tsx +7 -4
  54. package/src/audio-player.test.tsx +78 -0
  55. package/src/audio-player.tsx +243 -56
  56. package/src/audio-visualizer.stories.tsx +126 -0
  57. package/src/audio-visualizer.test.tsx +438 -0
  58. package/src/audio-visualizer.tsx +367 -0
  59. package/src/canvas.stories.tsx +150 -1
  60. package/src/chat-shell.stories.tsx +18 -3
  61. package/src/chat.stories.tsx +16 -2
  62. package/src/code-block.stories.tsx +9 -1
  63. package/src/code-block.test.tsx +100 -1
  64. package/src/code-block.tsx +166 -103
  65. package/src/commit.tsx +30 -41
  66. package/src/composer.stories.tsx +372 -9
  67. package/src/composer.test.tsx +357 -6
  68. package/src/composer.tsx +217 -35
  69. package/src/confirmation.stories.tsx +72 -1
  70. package/src/confirmation.test.tsx +216 -2
  71. package/src/confirmation.tsx +264 -4
  72. package/src/context-panel.stories.tsx +9 -1
  73. package/src/context-panel.tsx +3 -2
  74. package/src/conversation.stories.tsx +86 -2
  75. package/src/conversation.test.tsx +66 -0
  76. package/src/conversation.tsx +52 -9
  77. package/src/diff-view.stories.tsx +196 -0
  78. package/src/diff-view.test.tsx +238 -0
  79. package/src/diff-view.tsx +697 -0
  80. package/src/environment-variables.tsx +20 -37
  81. package/src/file-tree.test.tsx +21 -0
  82. package/src/file-tree.tsx +12 -2
  83. package/src/gallery.tsx +4 -2
  84. package/src/index.ts +14 -4
  85. package/src/inline-citation.tsx +5 -5
  86. package/src/jsx-preview.stories.tsx +2 -2
  87. package/src/jsx-preview.tsx +151 -43
  88. package/src/markdown-view.stories.tsx +92 -1
  89. package/src/markdown-view.test.tsx +237 -2
  90. package/src/markdown-view.tsx +157 -6
  91. package/src/message-compare.stories.tsx +175 -0
  92. package/src/message-compare.test.tsx +207 -0
  93. package/src/message-compare.tsx +453 -0
  94. package/src/message-form.stories.tsx +65 -4
  95. package/src/message-form.test.tsx +8 -2
  96. package/src/message-form.tsx +15 -7
  97. package/src/message-table.stories.tsx +2 -2
  98. package/src/message-table.test.tsx +7 -0
  99. package/src/message-table.tsx +8 -4
  100. package/src/message.stories.tsx +9 -1
  101. package/src/message.test.tsx +247 -1
  102. package/src/message.tsx +125 -15
  103. package/src/microcopy.test.tsx +40 -0
  104. package/src/{model-selector.stories.tsx → model-provider-logo.stories.tsx} +17 -8
  105. package/src/model-provider-logo.test.tsx +109 -0
  106. package/src/model-provider-logo.tsx +183 -0
  107. package/src/open-in-chat.tsx +50 -29
  108. package/src/package-info.tsx +12 -12
  109. package/src/permission-mode-select.stories.tsx +82 -0
  110. package/src/permission-mode-select.test.tsx +100 -0
  111. package/src/permission-mode-select.tsx +137 -0
  112. package/src/persona-missing-peer.test.tsx +54 -0
  113. package/src/persona.tsx +68 -22
  114. package/src/plan.stories.tsx +166 -0
  115. package/src/plan.test.tsx +267 -0
  116. package/src/plan.tsx +182 -20
  117. package/src/prompt-input-effort.stories.tsx +123 -0
  118. package/src/prompt-input-effort.test.tsx +83 -0
  119. package/src/prompt-input-effort.tsx +136 -0
  120. package/src/prompt-input-mode.stories.tsx +108 -0
  121. package/src/prompt-input-mode.test.tsx +99 -0
  122. package/src/prompt-input-mode.tsx +169 -0
  123. package/src/prompt-input-slash.stories.tsx +211 -0
  124. package/src/prompt-input-slash.test.tsx +262 -0
  125. package/src/prompt-input-slash.tsx +541 -0
  126. package/src/prompt-input.stories.tsx +2 -2
  127. package/src/prompt-input.test.tsx +67 -1
  128. package/src/prompt-input.tsx +42 -4
  129. package/src/queue.tsx +4 -4
  130. package/src/reasoning.tsx +42 -17
  131. package/src/sandbox.stories.tsx +9 -1
  132. package/src/sandbox.tsx +3 -3
  133. package/src/schema-display.test.tsx +56 -0
  134. package/src/schema-display.tsx +71 -37
  135. package/src/selection-toolbar.stories.tsx +9 -1
  136. package/src/session-header.stories.tsx +128 -0
  137. package/src/session-header.test.tsx +138 -0
  138. package/src/session-header.tsx +243 -0
  139. package/src/session-status-bar.stories.tsx +73 -0
  140. package/src/session-status-bar.test.tsx +94 -0
  141. package/src/session-status-bar.tsx +165 -0
  142. package/src/snippet.stories.tsx +9 -1
  143. package/src/snippet.test.tsx +6 -2
  144. package/src/snippet.tsx +14 -34
  145. package/src/speech-input.test.tsx +109 -0
  146. package/src/speech-input.tsx +31 -3
  147. package/src/stack-trace.tsx +24 -36
  148. package/src/streamdown-i18n.test.tsx +24 -1
  149. package/src/task.stories.tsx +13 -3
  150. package/src/templates-ai-assistant.stories.tsx +21 -1
  151. package/src/test-results.tsx +47 -27
  152. package/src/token-usage.stories.tsx +24 -0
  153. package/src/token-usage.test.tsx +92 -0
  154. package/src/{context.tsx → token-usage.tsx} +73 -59
  155. package/src/tool-result-card.stories.tsx +9 -1
  156. package/src/tool.stories.tsx +13 -3
  157. package/src/tool.test.tsx +65 -0
  158. package/src/tool.tsx +73 -30
  159. package/src/transcription.tsx +1 -1
  160. package/src/turn-status.stories.tsx +124 -0
  161. package/src/turn-status.test.tsx +74 -0
  162. package/src/turn-status.tsx +174 -0
  163. package/src/use-audio-level.ts +104 -0
  164. package/src/voice-selector.tsx +5 -5
  165. package/src/web-preview.test.tsx +51 -1
  166. package/src/web-preview.tsx +46 -10
  167. package/dist/_audio-player-media-chrome-KA5DY54G.js.map +0 -1
  168. package/dist/_flow-boundary-D63PJ65S.js.map +0 -1
  169. package/dist/_interactive-terminal-xterm-ATJ5EW3G.js +0 -11
  170. package/dist/_interactive-terminal-xterm-ATJ5EW3G.js.map +0 -1
  171. package/dist/_persona-rive-RFR2EUWP.js.map +0 -1
  172. package/src/_interactive-terminal-xterm.ts +0 -32
  173. package/src/blocks-ai-composer.stories.tsx +0 -83
  174. package/src/context.stories.tsx +0 -16
  175. package/src/interactive-terminal.stories.tsx +0 -165
  176. package/src/interactive-terminal.test.tsx +0 -448
  177. package/src/interactive-terminal.tsx +0 -444
  178. package/src/model-selector.test.tsx +0 -55
  179. package/src/model-selector.tsx +0 -225
  180. package/src/terminal.tsx +0 -244
@@ -1,30 +1,39 @@
1
1
  /**
2
- * ModelSelectora command-palette model picker.
2
+ * ModelProviderLogothe mark of an AI model provider, sized for a list row.
3
3
  *
4
- * `ModelSelectorLogo` fetches provider marks from a **remote origin**
4
+ * It fetches provider marks from a **remote origin**
5
5
  * (`models.dev`) by default, so a restrictive `img-src` blocks them. Pass `src`
6
6
  * to self-host, or `fallback` to control what a blocked load renders — it is
7
7
  * never a broken-image glyph. See `docs/CSP-AND-NETWORK.md`.
8
8
  */
9
9
  import type { Meta, StoryObj } from "@storybook/react-vite";
10
10
 
11
- import { ModelSelectorLogo } from "./model-selector";
11
+ import { ModelProviderLogo } from "./model-provider-logo";
12
12
 
13
13
  const meta = {
14
- title: "AI/ModelSelectorLogo",
15
- component: ModelSelectorLogo,
14
+ title: "AI/ModelProviderLogo",
15
+ component: ModelProviderLogo,
16
16
  parameters: {
17
17
  layout: "centered",
18
18
  docs: {
19
19
  description: {
20
20
  component:
21
- "Provider logo for the model selector. Defaults to fetching from models.dev; pass `src` for a self-hosted asset and `fallback` for the blocked/offline case. A failed load renders a neutral glyph, never a broken image.",
21
+ "The mark of an AI model provider, sized for a row in a model list. Defaults to " +
22
+ "fetching from models.dev; pass `src` for a self-hosted asset and `fallback` for the " +
23
+ "blocked/offline case. A failed load renders a neutral glyph, never a broken image.\n\n" +
24
+ "**`ModelProviderLogo` vs `ServiceLogo` (`@elabs-ai/components-icons`).** An AI " +
25
+ "provider mark with a zero-config remote default → this component. Any other " +
26
+ "third-party service mark, resolved from a registry the app supplies and never " +
27
+ "fetched → `ServiceLogo`.\n\n" +
28
+ "For the list this logo sits in: an inline pill in a composer footer → `ModelPicker` " +
29
+ "(`@elabs-ai/components-ui`); a full ⌘K palette → compose `CommandDialog` + " +
30
+ "`Command*` from `@elabs-ai/components-ui` directly.",
22
31
  },
23
32
  },
24
33
  },
25
34
  args: { provider: "anthropic" },
26
35
  tags: ["autodocs"],
27
- } satisfies Meta<typeof ModelSelectorLogo>;
36
+ } satisfies Meta<typeof ModelProviderLogo>;
28
37
 
29
38
  export default meta;
30
39
  type Story = StoryObj<typeof meta>;
@@ -64,7 +73,7 @@ export const LiveRemoteLogos: Story = {
64
73
  <div className="flex items-center gap-4">
65
74
  {(["anthropic", "openai", "google", "mistral"] as const).map((provider) => (
66
75
  <figure className="flex flex-col items-center gap-2" key={provider}>
67
- <ModelSelectorLogo provider={provider} />
76
+ <ModelProviderLogo provider={provider} />
68
77
  <figcaption className="text-meta text-muted-foreground">{provider}</figcaption>
69
78
  </figure>
70
79
  ))}
@@ -0,0 +1,109 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
3
+
4
+ import {
5
+ MODEL_PROVIDER_LOGO_BASE_URL,
6
+ ModelProviderLogo,
7
+ ModelProviderLogoGroup,
8
+ } from "./model-provider-logo";
9
+
10
+ afterEach(() => {
11
+ cleanup();
12
+ document.documentElement.removeAttribute("data-theme");
13
+ document.documentElement.style.colorScheme = "";
14
+ });
15
+
16
+ describe("ModelProviderLogo — remote origin escape hatches", () => {
17
+ it("defaults to the models.dev logo for the provider", () => {
18
+ render(<ModelProviderLogo provider="anthropic" />);
19
+ expect(screen.getByRole("img", { name: "anthropic logo" })).toHaveAttribute(
20
+ "src",
21
+ `${MODEL_PROVIDER_LOGO_BASE_URL}/anthropic.svg`,
22
+ );
23
+ });
24
+
25
+ it("lets a caller override src for a self-hosted asset", () => {
26
+ // The old props type did `Omit<…, "src">` AND spread props before `src=`, so
27
+ // a self-hosting consumer had no way to win. Both are fixed.
28
+ render(<ModelProviderLogo provider="anthropic" src="/local/anthropic.svg" />);
29
+ expect(screen.getByRole("img", { name: "anthropic logo" })).toHaveAttribute(
30
+ "src",
31
+ "/local/anthropic.svg",
32
+ );
33
+ });
34
+
35
+ it("renders a neutral glyph instead of a broken image when the load fails", () => {
36
+ render(<ModelProviderLogo provider="anthropic" />);
37
+ fireEvent.error(screen.getByRole("img", { name: "anthropic logo" }));
38
+
39
+ const fallback = screen.getByRole("img", { name: "anthropic logo" });
40
+ expect(fallback.tagName.toLowerCase()).toBe("svg");
41
+ expect(fallback).not.toHaveAttribute("src");
42
+ });
43
+
44
+ it("renders a caller-supplied fallback when provided", () => {
45
+ render(<ModelProviderLogo provider="anthropic" fallback={<span data-testid="fb">AI</span>} />);
46
+ fireEvent.error(screen.getByRole("img", { name: "anthropic logo" }));
47
+ expect(screen.getByTestId("fb")).toBeInTheDocument();
48
+ });
49
+
50
+ it("still calls a caller's onError", () => {
51
+ const onError = vi.fn();
52
+ render(<ModelProviderLogo provider="anthropic" onError={onError} />);
53
+ fireEvent.error(screen.getByRole("img", { name: "anthropic logo" }));
54
+ expect(onError).toHaveBeenCalledTimes(1);
55
+ });
56
+
57
+ it("keeps an accessible name in both states", () => {
58
+ const { rerender } = render(<ModelProviderLogo provider="openai" />);
59
+ expect(screen.getByRole("img", { name: "openai logo" })).toBeInTheDocument();
60
+
61
+ fireEvent.error(screen.getByRole("img", { name: "openai logo" }));
62
+ rerender(<ModelProviderLogo provider="openai" />);
63
+ expect(screen.getByRole("img", { name: "openai logo" })).toBeInTheDocument();
64
+ });
65
+ });
66
+
67
+ // Was `dark:invert` — a bare Tailwind dark: variant, which per theming.md only
68
+ // covers the two SHIPPED themes and misses any consumer-registered dark theme
69
+ // (ADR 0029, open theme registry). Now driven by `resolveThemeIsDark`, the
70
+ // same signal `Toaster` uses, so it follows the ACTIVE theme's own
71
+ // `color-scheme`, not a hardcoded theme name (review #1).
72
+ describe("ModelProviderLogo — theme-driven invert (review #1)", () => {
73
+ it("does not invert under the light theme", () => {
74
+ document.documentElement.setAttribute("data-theme", "light");
75
+ render(<ModelProviderLogo provider="anthropic" />);
76
+ expect(screen.getByRole("img", { name: "anthropic logo" })).not.toHaveClass("invert");
77
+ });
78
+
79
+ it("inverts under the dark theme", () => {
80
+ document.documentElement.setAttribute("data-theme", "dark");
81
+ render(<ModelProviderLogo provider="anthropic" />);
82
+ expect(screen.getByRole("img", { name: "anthropic logo" })).toHaveClass("invert");
83
+ });
84
+
85
+ it("also inverts under a consumer-registered dark theme, not only the shipped one", () => {
86
+ // A raw `dark:` utility only ever matches `data-theme="dark"` — this
87
+ // is exactly the gap `resolveThemeIsDark`'s `color-scheme` reading closes.
88
+ document.documentElement.setAttribute("data-theme", "midnight");
89
+ document.documentElement.style.colorScheme = "dark";
90
+ render(<ModelProviderLogo provider="anthropic" />);
91
+ expect(screen.getByRole("img", { name: "anthropic logo" })).toHaveClass("invert");
92
+ });
93
+ });
94
+
95
+ describe("ModelProviderLogoGroup — theme-driven backdrop (review #1)", () => {
96
+ it("backs logos with --background in the light theme", () => {
97
+ document.documentElement.setAttribute("data-theme", "light");
98
+ const { container } = render(<ModelProviderLogoGroup />);
99
+ expect(container.firstElementChild?.className).toContain("[&>img]:bg-background");
100
+ expect(container.firstElementChild?.className).not.toContain("[&>img]:bg-foreground");
101
+ });
102
+
103
+ it("backs logos with --foreground in the dark theme", () => {
104
+ document.documentElement.setAttribute("data-theme", "dark");
105
+ const { container } = render(<ModelProviderLogoGroup />);
106
+ expect(container.firstElementChild?.className).toContain("[&>img]:bg-foreground");
107
+ expect(container.firstElementChild?.className).not.toContain("[&>img]:bg-background");
108
+ });
109
+ });
@@ -0,0 +1,183 @@
1
+ "use client";
2
+
3
+ import { cn } from "@elabs-ai/components-ui/lib/cn";
4
+ import { resolveThemeIsDark } from "@elabs-ai/components-tokens";
5
+ import { BotIcon } from "lucide-react";
6
+ import type { ComponentProps, ReactNode } from "react";
7
+ import { useEffect, useState } from "react";
8
+
9
+ /**
10
+ * Tracks the ACTIVE theme's own `color-scheme` (`resolveThemeIsDark`), not the
11
+ * two shipped `light`/`dark` themes — a consumer-authored dark theme still
12
+ * gets an inverted (visible) logo with no registration step (theming.md:
13
+ * "Use semantic tokens, not `dark:` — `@custom-variant dark` covers only the
14
+ * themes shipped HERE"). Mirrors `Toaster`'s `useDocumentThemeMode`
15
+ * (`packages/ui/src/components/sonner/sonner.tsx`).
16
+ */
17
+ function useIsDarkTheme(): boolean {
18
+ const [isDark, setIsDark] = useState(false);
19
+ useEffect(() => {
20
+ if (typeof document === "undefined") return;
21
+ const el = document.documentElement;
22
+ const read = () => setIsDark(resolveThemeIsDark(el));
23
+ read();
24
+ const obs = new MutationObserver(read);
25
+ obs.observe(el, { attributes: true, attributeFilter: ["data-theme"] });
26
+ return () => obs.disconnect();
27
+ }, []);
28
+ return isDark;
29
+ }
30
+
31
+ /**
32
+ * Where provider logos are fetched from by default.
33
+ *
34
+ * This is a REMOTE origin: a deployment with a restrictive `img-src` blocks it.
35
+ * Self-host the SVGs and pass `src` (or set a `fallback`) — see
36
+ * `docs/CSP-AND-NETWORK.md`.
37
+ */
38
+ export const MODEL_PROVIDER_LOGO_BASE_URL = "https://models.dev/logos";
39
+
40
+ export type ModelProviderLogoProps = Omit<ComponentProps<"img">, "alt"> & {
41
+ /**
42
+ * Override the logo URL — point at a self-hosted or bundled asset when a CSP
43
+ * blocks `models.dev`. Defaults to `${MODEL_PROVIDER_LOGO_BASE_URL}/<provider>.svg`.
44
+ */
45
+ src?: string;
46
+ /**
47
+ * Rendered when the logo fails to load (blocked, offline, or unknown
48
+ * provider). Defaults to a neutral Lucide glyph, so the row never collapses
49
+ * to a broken image.
50
+ */
51
+ fallback?: ReactNode;
52
+ provider:
53
+ | "moonshotai-cn"
54
+ | "lucidquery"
55
+ | "moonshotai"
56
+ | "zai-coding-plan"
57
+ | "alibaba"
58
+ | "xai"
59
+ | "vultr"
60
+ | "nvidia"
61
+ | "upstage"
62
+ | "groq"
63
+ | "github-copilot"
64
+ | "mistral"
65
+ | "vercel"
66
+ | "nebius"
67
+ | "deepseek"
68
+ | "alibaba-cn"
69
+ | "google-vertex-anthropic"
70
+ | "venice"
71
+ | "chutes"
72
+ | "cortecs"
73
+ | "github-models"
74
+ | "togetherai"
75
+ | "azure"
76
+ | "baseten"
77
+ | "huggingface"
78
+ | "opencode"
79
+ | "fastrouter"
80
+ | "google"
81
+ | "google-vertex"
82
+ | "cloudflare-workers-ai"
83
+ | "inception"
84
+ | "wandb"
85
+ | "openai"
86
+ | "zhipuai-coding-plan"
87
+ | "perplexity"
88
+ | "openrouter"
89
+ | "zenmux"
90
+ | "v0"
91
+ | "iflowcn"
92
+ | "synthetic"
93
+ | "deepinfra"
94
+ | "zhipuai"
95
+ | "submodel"
96
+ | "zai"
97
+ | "inference"
98
+ | "requesty"
99
+ | "morph"
100
+ | "lmstudio"
101
+ | "anthropic"
102
+ | "aihubmix"
103
+ | "fireworks-ai"
104
+ | "modelscope"
105
+ | "llama"
106
+ | "scaleway"
107
+ | "amazon-bedrock"
108
+ | "cerebras"
109
+ // oxlint-disable-next-line typescript-eslint(ban-types) -- intentional pattern for autocomplete-friendly string union
110
+ | (string & {});
111
+ };
112
+
113
+ /**
114
+ * The mark of an AI model provider (`anthropic`, `openai`, `google`, …), sized
115
+ * for a row in a model list.
116
+ *
117
+ * **`ModelProviderLogo` vs `ServiceLogo` (`@elabs-ai/components-icons`).** An AI
118
+ * provider mark with a zero-config remote default — name the provider and a logo
119
+ * appears, no registration step — is this component. Any other third-party
120
+ * service mark, resolved from a registry the app supplies and never fetched, is
121
+ * `ServiceLogo`. The two are not interchangeable: `ServiceLogo` has no
122
+ * per-instance `src` and no `onError`, and this one reaches a remote origin by
123
+ * default (see `docs/CSP-AND-NETWORK.md`).
124
+ */
125
+ export const ModelProviderLogo = ({
126
+ provider,
127
+ className,
128
+ src,
129
+ fallback,
130
+ onError,
131
+ ...props
132
+ }: ModelProviderLogoProps) => {
133
+ const [failed, setFailed] = useState(false);
134
+ const isDark = useIsDarkTheme();
135
+
136
+ // A blocked/missing logo must not leave a broken-image glyph in the row.
137
+ if (failed) {
138
+ return (
139
+ <>
140
+ {fallback ?? (
141
+ <BotIcon aria-label={`${provider} logo`} className={cn("size-3", className)} role="img" />
142
+ )}
143
+ </>
144
+ );
145
+ }
146
+
147
+ return (
148
+ <img
149
+ {...props}
150
+ alt={`${provider} logo`}
151
+ className={cn("size-3", isDark && "invert", className)}
152
+ height={12}
153
+ onError={(event) => {
154
+ setFailed(true);
155
+ onError?.(event);
156
+ }}
157
+ // AFTER the spread, so the caller's `src` wins via the destructured
158
+ // default rather than being silently overwritten.
159
+ src={src ?? `${MODEL_PROVIDER_LOGO_BASE_URL}/${provider}.svg`}
160
+ width={12}
161
+ />
162
+ );
163
+ };
164
+
165
+ export type ModelProviderLogoGroupProps = ComponentProps<"div">;
166
+
167
+ export const ModelProviderLogoGroup = ({ className, ...props }: ModelProviderLogoGroupProps) => {
168
+ // Same active-theme signal as the single logo above (not the two shipped
169
+ // `light`/`dark` themes) — the backdrop swaps to `--foreground` so a
170
+ // transparent-background SVG still reads against a dark chip in ANY dark
171
+ // theme a consumer registers.
172
+ const isDark = useIsDarkTheme();
173
+ return (
174
+ <div
175
+ className={cn(
176
+ "flex shrink-0 items-center -space-x-1 [&>img]:rounded-full [&>img]:p-px [&>img]:ring-1",
177
+ isDark ? "[&>img]:bg-foreground" : "[&>img]:bg-background",
178
+ className,
179
+ )}
180
+ {...props}
181
+ />
182
+ );
183
+ };
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button } from "@elabs-ai/components-ui";
3
+ import { Button, useLocale } from "@elabs-ai/components-ui";
4
4
  import {
5
5
  DropdownMenu,
6
6
  DropdownMenuContent,
@@ -23,11 +23,11 @@ const providers = {
23
23
  })}`,
24
24
  icon: (
25
25
  <svg fill="currentColor" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
26
- <title>OpenAI</title>
26
+ <title>OpenAI</title> {/* i18n-exempt: brand name */}
27
27
  <path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
28
28
  </svg>
29
29
  ),
30
- title: "Open in ChatGPT",
30
+ title: "ChatGPT", // i18n-exempt: brand name
31
31
  },
32
32
  claude: {
33
33
  createUrl: (q: string) =>
@@ -36,7 +36,7 @@ const providers = {
36
36
  })}`,
37
37
  icon: (
38
38
  <svg fill="currentColor" role="img" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
39
- <title>Claude</title>
39
+ <title>Claude</title> {/* i18n-exempt: brand name */}
40
40
  <path
41
41
  clipRule="evenodd"
42
42
  d="M2.3545 7.9775L4.7145 6.654L4.7545 6.539L4.7145 6.475H4.6L4.205 6.451L2.856 6.4145L1.6865 6.366L0.5535 6.305L0.268 6.2445L0 5.892L0.0275 5.716L0.2675 5.5555L0.6105 5.5855L1.3705 5.637L2.5095 5.716L3.3355 5.7645L4.56 5.892H4.7545L4.782 5.8135L4.715 5.7645L4.6635 5.716L3.4845 4.918L2.2085 4.074L1.5405 3.588L1.1785 3.3425L0.9965 3.1115L0.9175 2.6075L1.2455 2.2465L1.686 2.2765L1.7985 2.307L2.245 2.65L3.199 3.388L4.4445 4.3045L4.627 4.4565L4.6995 4.405L4.709 4.3685L4.627 4.2315L3.9495 3.0085L3.2265 1.7635L2.9045 1.2475L2.8195 0.938C2.78711 0.819128 2.76965 0.696687 2.7675 0.5735L3.1415 0.067L3.348 0L3.846 0.067L4.056 0.249L4.366 0.956L4.867 2.0705L5.6445 3.5855L5.8725 4.0345L5.994 4.4505L6.0395 4.578H6.1185V4.505L6.1825 3.652L6.301 2.6045L6.416 1.257L6.456 0.877L6.644 0.422L7.0175 0.176L7.3095 0.316L7.5495 0.6585L7.516 0.8805L7.373 1.806L7.0935 3.2575L6.9115 4.2285H7.0175L7.139 4.1075L7.6315 3.4545L8.4575 2.4225L8.8225 2.0125L9.2475 1.5605L9.521 1.345H10.0375L10.4175 1.9095L10.2475 2.4925L9.7155 3.166L9.275 3.737L8.643 4.587L8.248 5.267L8.2845 5.322L8.3785 5.312L9.8065 5.009L10.578 4.869L11.4985 4.7115L11.915 4.9055L11.9605 5.103L11.7965 5.5065L10.812 5.7495L9.6575 5.9805L7.938 6.387L7.917 6.402L7.9415 6.4325L8.716 6.5055L9.047 6.5235H9.858L11.368 6.636L11.763 6.897L12 7.216L11.9605 7.4585L11.353 7.7685L10.533 7.574L8.6185 7.119L7.9625 6.9545H7.8715V7.0095L8.418 7.5435L9.421 8.4485L10.6755 9.6135L10.739 9.9025L10.578 10.13L10.408 10.1055L9.3055 9.277L8.88 8.9035L7.917 8.0935H7.853V8.1785L8.075 8.503L9.2475 10.2635L9.3085 10.8035L9.2235 10.98L8.9195 11.0865L8.5855 11.0255L7.8985 10.063L7.191 8.9795L6.6195 8.008L6.5495 8.048L6.2125 11.675L6.0545 11.86L5.69 12L5.3865 11.7695L5.2255 11.396L5.3865 10.658L5.581 9.696L5.7385 8.931L5.8815 7.981L5.9665 7.665L5.9605 7.644L5.8905 7.653L5.1735 8.6365L4.0835 10.109L3.2205 11.0315L3.0135 11.1135L2.655 10.9285L2.6885 10.5975L2.889 10.303L4.083 8.785L4.803 7.844L5.268 7.301L5.265 7.222H5.2375L2.066 9.28L1.501 9.353L1.2575 9.125L1.288 8.752L1.4035 8.6305L2.3575 7.9745L2.3545 7.9775Z"
@@ -44,7 +44,7 @@ const providers = {
44
44
  />
45
45
  </svg>
46
46
  ),
47
- title: "Open in Claude",
47
+ title: "Claude", // i18n-exempt: brand name
48
48
  },
49
49
  cursor: {
50
50
  createUrl: (text: string) => {
@@ -54,24 +54,24 @@ const providers = {
54
54
  },
55
55
  icon: (
56
56
  <svg version="1.1" viewBox="0 0 466.73 532.09" xmlns="http://www.w3.org/2000/svg">
57
- <title>Cursor</title>
57
+ <title>Cursor</title> {/* i18n-exempt: brand name */}
58
58
  <path
59
59
  d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"
60
60
  fill="currentColor"
61
61
  />
62
62
  </svg>
63
63
  ),
64
- title: "Open in Cursor",
64
+ title: "Cursor", // i18n-exempt: brand name
65
65
  },
66
66
  github: {
67
67
  createUrl: (url: string) => url,
68
68
  icon: (
69
69
  <svg fill="currentColor" role="img" viewBox="0 0 24 24">
70
- <title>GitHub</title>
70
+ <title>GitHub</title> {/* i18n-exempt: brand name */}
71
71
  <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
72
72
  </svg>
73
73
  ),
74
- title: "Open in GitHub",
74
+ title: "GitHub", // i18n-exempt: brand name
75
75
  },
76
76
  scira: {
77
77
  createUrl: (q: string) =>
@@ -86,7 +86,7 @@ const providers = {
86
86
  width="910"
87
87
  xmlns="http://www.w3.org/2000/svg"
88
88
  >
89
- <title>Scira AI</title>
89
+ <title>Scira AI</title> {/* i18n-exempt: brand name */}
90
90
  <path
91
91
  d="M647.664 197.775C569.13 189.049 525.5 145.419 516.774 66.8849C508.048 145.419 464.418 189.049 385.884 197.775C464.418 206.501 508.048 250.131 516.774 328.665C525.5 250.131 569.13 206.501 647.664 197.775Z"
92
92
  fill="currentColor"
@@ -136,7 +136,7 @@ const providers = {
136
136
  />
137
137
  </svg>
138
138
  ),
139
- title: "Open in Scira",
139
+ title: "Scira", // i18n-exempt: brand name
140
140
  },
141
141
  t3: {
142
142
  createUrl: (q: string) =>
@@ -144,7 +144,7 @@ const providers = {
144
144
  q,
145
145
  })}`,
146
146
  icon: <MessageCircleIcon />,
147
- title: "Open in T3 Chat",
147
+ title: "T3 Chat", // i18n-exempt: brand name
148
148
  },
149
149
  v0: {
150
150
  createUrl: (q: string) =>
@@ -158,7 +158,7 @@ const providers = {
158
158
  <path d="M147 56H133V23.9531L100.953 56H133V70H96.6875C85.8144 70 77 61.1856 77 50.3125V14H91V46.1562L123.156 14H91V0H127.312C138.186 0 147 8.81439 147 19.6875V56Z" />
159
159
  </svg>
160
160
  ),
161
- title: "Open in v0",
161
+ title: "v0", // i18n-exempt: brand name
162
162
  },
163
163
  };
164
164
 
@@ -208,21 +208,25 @@ export const OpenInSeparator = (props: OpenInSeparatorProps) => (
208
208
 
209
209
  export type OpenInTriggerProps = ComponentProps<typeof DropdownMenuTrigger>;
210
210
 
211
- export const OpenInTrigger = ({ children, ...props }: OpenInTriggerProps) => (
212
- <DropdownMenuTrigger {...props} asChild>
213
- {children ?? (
214
- <Button type="button" variant="outline">
215
- Open in chat
216
- <ChevronDownIcon className="size-4" />
217
- </Button>
218
- )}
219
- </DropdownMenuTrigger>
220
- );
211
+ export const OpenInTrigger = ({ children, ...props }: OpenInTriggerProps) => {
212
+ const { t } = useLocale();
213
+ return (
214
+ <DropdownMenuTrigger {...props} asChild>
215
+ {children ?? (
216
+ <Button type="button" variant="outline">
217
+ {t("ai.openInChat.trigger")}
218
+ <ChevronDownIcon className="size-4" />
219
+ </Button>
220
+ )}
221
+ </DropdownMenuTrigger>
222
+ );
223
+ };
221
224
 
222
225
  export type OpenInChatGPTProps = ComponentProps<typeof DropdownMenuItem>;
223
226
 
224
227
  export const OpenInChatGPT = (props: OpenInChatGPTProps) => {
225
228
  const { query } = useOpenInContext();
229
+ const { t } = useLocale();
226
230
  return (
227
231
  <DropdownMenuItem asChild {...props}>
228
232
  <a
@@ -232,7 +236,9 @@ export const OpenInChatGPT = (props: OpenInChatGPTProps) => {
232
236
  target="_blank"
233
237
  >
234
238
  <span className="shrink-0">{providers.chatgpt.icon}</span>
235
- <span className="flex-1">{providers.chatgpt.title}</span>
239
+ <span className="flex-1">
240
+ {t("ai.openInChat.openInProvider", { provider: providers.chatgpt.title })}
241
+ </span>
236
242
  <ExternalLinkIcon className="size-4 shrink-0" />
237
243
  </a>
238
244
  </DropdownMenuItem>
@@ -243,6 +249,7 @@ export type OpenInClaudeProps = ComponentProps<typeof DropdownMenuItem>;
243
249
 
244
250
  export const OpenInClaude = (props: OpenInClaudeProps) => {
245
251
  const { query } = useOpenInContext();
252
+ const { t } = useLocale();
246
253
  return (
247
254
  <DropdownMenuItem asChild {...props}>
248
255
  <a
@@ -252,7 +259,9 @@ export const OpenInClaude = (props: OpenInClaudeProps) => {
252
259
  target="_blank"
253
260
  >
254
261
  <span className="shrink-0">{providers.claude.icon}</span>
255
- <span className="flex-1">{providers.claude.title}</span>
262
+ <span className="flex-1">
263
+ {t("ai.openInChat.openInProvider", { provider: providers.claude.title })}
264
+ </span>
256
265
  <ExternalLinkIcon className="size-4 shrink-0" />
257
266
  </a>
258
267
  </DropdownMenuItem>
@@ -263,6 +272,7 @@ export type OpenInT3Props = ComponentProps<typeof DropdownMenuItem>;
263
272
 
264
273
  export const OpenInT3 = (props: OpenInT3Props) => {
265
274
  const { query } = useOpenInContext();
275
+ const { t } = useLocale();
266
276
  return (
267
277
  <DropdownMenuItem asChild {...props}>
268
278
  <a
@@ -272,7 +282,9 @@ export const OpenInT3 = (props: OpenInT3Props) => {
272
282
  target="_blank"
273
283
  >
274
284
  <span className="shrink-0">{providers.t3.icon}</span>
275
- <span className="flex-1">{providers.t3.title}</span>
285
+ <span className="flex-1">
286
+ {t("ai.openInChat.openInProvider", { provider: providers.t3.title })}
287
+ </span>
276
288
  <ExternalLinkIcon className="size-4 shrink-0" />
277
289
  </a>
278
290
  </DropdownMenuItem>
@@ -283,6 +295,7 @@ export type OpenInSciraProps = ComponentProps<typeof DropdownMenuItem>;
283
295
 
284
296
  export const OpenInScira = (props: OpenInSciraProps) => {
285
297
  const { query } = useOpenInContext();
298
+ const { t } = useLocale();
286
299
  return (
287
300
  <DropdownMenuItem asChild {...props}>
288
301
  <a
@@ -292,7 +305,9 @@ export const OpenInScira = (props: OpenInSciraProps) => {
292
305
  target="_blank"
293
306
  >
294
307
  <span className="shrink-0">{providers.scira.icon}</span>
295
- <span className="flex-1">{providers.scira.title}</span>
308
+ <span className="flex-1">
309
+ {t("ai.openInChat.openInProvider", { provider: providers.scira.title })}
310
+ </span>
296
311
  <ExternalLinkIcon className="size-4 shrink-0" />
297
312
  </a>
298
313
  </DropdownMenuItem>
@@ -303,6 +318,7 @@ export type OpenInv0Props = ComponentProps<typeof DropdownMenuItem>;
303
318
 
304
319
  export const OpenInv0 = (props: OpenInv0Props) => {
305
320
  const { query } = useOpenInContext();
321
+ const { t } = useLocale();
306
322
  return (
307
323
  <DropdownMenuItem asChild {...props}>
308
324
  <a
@@ -312,7 +328,9 @@ export const OpenInv0 = (props: OpenInv0Props) => {
312
328
  target="_blank"
313
329
  >
314
330
  <span className="shrink-0">{providers.v0.icon}</span>
315
- <span className="flex-1">{providers.v0.title}</span>
331
+ <span className="flex-1">
332
+ {t("ai.openInChat.openInProvider", { provider: providers.v0.title })}
333
+ </span>
316
334
  <ExternalLinkIcon className="size-4 shrink-0" />
317
335
  </a>
318
336
  </DropdownMenuItem>
@@ -323,6 +341,7 @@ export type OpenInCursorProps = ComponentProps<typeof DropdownMenuItem>;
323
341
 
324
342
  export const OpenInCursor = (props: OpenInCursorProps) => {
325
343
  const { query } = useOpenInContext();
344
+ const { t } = useLocale();
326
345
  return (
327
346
  <DropdownMenuItem asChild {...props}>
328
347
  <a
@@ -332,7 +351,9 @@ export const OpenInCursor = (props: OpenInCursorProps) => {
332
351
  target="_blank"
333
352
  >
334
353
  <span className="shrink-0">{providers.cursor.icon}</span>
335
- <span className="flex-1">{providers.cursor.title}</span>
354
+ <span className="flex-1">
355
+ {t("ai.openInChat.openInProvider", { provider: providers.cursor.title })}
356
+ </span>
336
357
  <ExternalLinkIcon className="size-4 shrink-0" />
337
358
  </a>
338
359
  </DropdownMenuItem>
@@ -35,17 +35,17 @@ export const PackageInfoName = ({ className, children, ...props }: PackageInfoNa
35
35
  return (
36
36
  <div className={cn("flex items-center gap-2", className)} {...props}>
37
37
  <PackageIcon className="size-4 text-muted-foreground" />
38
- <span className="font-medium font-mono text-sm">{children ?? name}</span>
38
+ <span className="font-medium font-mono text-body">{children ?? name}</span>
39
39
  </div>
40
40
  );
41
41
  };
42
42
 
43
43
  const changeTypeStyles: Record<ChangeType, string> = {
44
- added: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400",
45
- major: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400",
46
- minor: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400",
47
- patch: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400",
48
- removed: "bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400",
44
+ added: "bg-info/10 text-info-text",
45
+ major: "bg-destructive/10 text-destructive-text",
46
+ minor: "bg-warning/10 text-warning-text",
47
+ patch: "bg-success/10 text-success-text",
48
+ removed: "bg-muted text-muted-foreground",
49
49
  };
50
50
 
51
51
  const changeTypeIcons: Record<ChangeType, React.ReactNode> = {
@@ -71,7 +71,7 @@ export const PackageInfoChangeType = ({
71
71
 
72
72
  return (
73
73
  <Badge
74
- className={cn("gap-1 text-xs capitalize", changeTypeStyles[changeType], className)}
74
+ className={cn("gap-1 text-meta capitalize", changeTypeStyles[changeType], className)}
75
75
  variant="secondary"
76
76
  {...props}
77
77
  >
@@ -93,7 +93,7 @@ export const PackageInfoVersion = ({ className, children, ...props }: PackageInf
93
93
  return (
94
94
  <div
95
95
  className={cn(
96
- "mt-2 flex items-center gap-2 font-mono text-muted-foreground text-sm",
96
+ "mt-2 flex items-center gap-2 font-mono text-muted-foreground text-body",
97
97
  className,
98
98
  )}
99
99
  {...props}
@@ -154,7 +154,7 @@ export const PackageInfoDescription = ({
154
154
  children,
155
155
  ...props
156
156
  }: PackageInfoDescriptionProps) => (
157
- <p className={cn("mt-2 text-muted-foreground text-sm", className)} {...props}>
157
+ <p className={cn("mt-2 text-muted-foreground text-body", className)} {...props}>
158
158
  {children}
159
159
  </p>
160
160
  );
@@ -175,7 +175,7 @@ export const PackageInfoDependencies = ({
175
175
  ...props
176
176
  }: PackageInfoDependenciesProps) => (
177
177
  <div className={cn("space-y-2", className)} {...props}>
178
- <span className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
178
+ <span className="font-medium text-muted-foreground text-meta uppercase tracking-wide">
179
179
  Dependencies
180
180
  </span>
181
181
  <div className="space-y-1">{children}</div>
@@ -194,11 +194,11 @@ export const PackageInfoDependency = ({
194
194
  children,
195
195
  ...props
196
196
  }: PackageInfoDependencyProps) => (
197
- <div className={cn("flex items-center justify-between text-sm", className)} {...props}>
197
+ <div className={cn("flex items-center justify-between text-body", className)} {...props}>
198
198
  {children ?? (
199
199
  <>
200
200
  <span className="font-mono text-muted-foreground">{name}</span>
201
- {version && <span className="font-mono text-xs">{version}</span>}
201
+ {version && <span className="font-mono text-meta">{version}</span>}
202
202
  </>
203
203
  )}
204
204
  </div>