@seed-ship/mcp-ui-solid 6.5.0 → 6.6.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 (83) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +37 -0
  3. package/dist/adapters/connector.cjs +112 -0
  4. package/dist/adapters/connector.cjs.map +1 -0
  5. package/dist/adapters/connector.d.ts +71 -0
  6. package/dist/adapters/connector.d.ts.map +1 -0
  7. package/dist/adapters/connector.js +112 -0
  8. package/dist/adapters/connector.js.map +1 -0
  9. package/dist/adapters/index.d.ts +18 -0
  10. package/dist/adapters/index.d.ts.map +1 -0
  11. package/dist/adapters.cjs +6 -0
  12. package/dist/adapters.cjs.map +1 -0
  13. package/dist/adapters.d.cts +18 -0
  14. package/dist/adapters.d.ts +18 -0
  15. package/dist/adapters.js +6 -0
  16. package/dist/adapters.js.map +1 -0
  17. package/dist/components/ExpandableWrapper.cjs +24 -6
  18. package/dist/components/ExpandableWrapper.cjs.map +1 -1
  19. package/dist/components/ExpandableWrapper.d.ts.map +1 -1
  20. package/dist/components/ExpandableWrapper.js +24 -6
  21. package/dist/components/ExpandableWrapper.js.map +1 -1
  22. package/dist/components/FeedbackInline.cjs +6 -2
  23. package/dist/components/FeedbackInline.cjs.map +1 -1
  24. package/dist/components/FeedbackInline.d.ts +2 -2
  25. package/dist/components/FeedbackInline.d.ts.map +1 -1
  26. package/dist/components/FeedbackInline.js +7 -3
  27. package/dist/components/FeedbackInline.js.map +1 -1
  28. package/dist/components/PresentationFeedback.cjs +207 -0
  29. package/dist/components/PresentationFeedback.cjs.map +1 -0
  30. package/dist/components/PresentationFeedback.d.ts +113 -0
  31. package/dist/components/PresentationFeedback.d.ts.map +1 -0
  32. package/dist/components/PresentationFeedback.js +207 -0
  33. package/dist/components/PresentationFeedback.js.map +1 -0
  34. package/dist/components/StreamingUIRenderer.cjs +82 -195
  35. package/dist/components/StreamingUIRenderer.cjs.map +1 -1
  36. package/dist/components/StreamingUIRenderer.d.ts +25 -5
  37. package/dist/components/StreamingUIRenderer.d.ts.map +1 -1
  38. package/dist/components/StreamingUIRenderer.js +84 -197
  39. package/dist/components/StreamingUIRenderer.js.map +1 -1
  40. package/dist/components/index.d.ts +2 -0
  41. package/dist/components/index.d.ts.map +1 -1
  42. package/dist/components.cjs +3 -0
  43. package/dist/components.cjs.map +1 -1
  44. package/dist/components.d.cts +2 -0
  45. package/dist/components.d.ts +2 -0
  46. package/dist/components.js +3 -0
  47. package/dist/components.js.map +1 -1
  48. package/dist/context/MCPUIStringsContext.cjs +38 -0
  49. package/dist/context/MCPUIStringsContext.cjs.map +1 -0
  50. package/dist/context/MCPUIStringsContext.d.ts +95 -0
  51. package/dist/context/MCPUIStringsContext.d.ts.map +1 -0
  52. package/dist/context/MCPUIStringsContext.js +38 -0
  53. package/dist/context/MCPUIStringsContext.js.map +1 -0
  54. package/dist/index.cjs +8 -0
  55. package/dist/index.cjs.map +1 -1
  56. package/dist/index.d.cts +5 -0
  57. package/dist/index.d.ts +5 -0
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +8 -0
  60. package/dist/index.js.map +1 -1
  61. package/dist/mcp-ui-spec/dist/schemas.cjs +103 -0
  62. package/dist/mcp-ui-spec/dist/schemas.cjs.map +1 -1
  63. package/dist/mcp-ui-spec/dist/schemas.js +103 -0
  64. package/dist/mcp-ui-spec/dist/schemas.js.map +1 -1
  65. package/docs/briefs/ROADMAP-opendata-macro-mcpui.md +912 -0
  66. package/package.json +17 -5
  67. package/src/adapters/connector.test.ts +165 -0
  68. package/src/adapters/connector.ts +234 -0
  69. package/src/adapters/index.ts +24 -0
  70. package/src/components/ExpandableWrapper.test.tsx +5 -2
  71. package/src/components/ExpandableWrapper.tsx +8 -6
  72. package/src/components/FeedbackInline.test.tsx +6 -3
  73. package/src/components/FeedbackInline.tsx +8 -6
  74. package/src/components/PresentationFeedback.test.tsx +163 -0
  75. package/src/components/PresentationFeedback.tsx +326 -0
  76. package/src/components/StreamingUIRenderer.parity.test.tsx +158 -0
  77. package/src/components/StreamingUIRenderer.tsx +42 -166
  78. package/src/components/index.ts +10 -0
  79. package/src/context/MCPUIStringsContext.test.tsx +116 -0
  80. package/src/context/MCPUIStringsContext.tsx +128 -0
  81. package/src/index.ts +27 -0
  82. package/tsconfig.tsbuildinfo +1 -1
  83. package/vite.config.ts +1 -0
@@ -1,8 +1,22 @@
1
1
  /**
2
- * StreamingUIRenderer Component - Phase 2
2
+ * StreamingUIRenderer Component
3
3
  *
4
- * Renders streaming dashboard components with skeleton states and progress indicators.
5
- * Uses the useStreamingUI hook for SSE connection and state management.
4
+ * Renders streaming dashboard components with skeleton states and progress
5
+ * indicators. Uses the `useStreamingUI` hook for SSE connection and state.
6
+ *
7
+ * ## Rendering parity (v6.6.0 — closes Gap 1 of ROADMAP-opendata-macro-mcpui)
8
+ *
9
+ * Each component received over SSE is delegated to the real
10
+ * `<UIResourceRenderer>`. Streamed `table` / `chart` / `map` / `action-group`
11
+ * therefore render with the SAME fidelity as a static layout — no more
12
+ * simplified "type + title" placeholder. Validation, telemetry, the error
13
+ * boundary and `errorMode` all come from `<UIResourceRenderer>`, so the two
14
+ * paths cannot drift.
15
+ *
16
+ * Delegation is a one-way value import (`UIResourceRenderer` never imports
17
+ * this file — no cycle). The streamed component's `position` is normalized
18
+ * to full-width before delegation : this component owns the 12-column grid,
19
+ * `<UIResourceRenderer>` only owns the component's own rendering.
6
20
  *
7
21
  * Features:
8
22
  * - Skeleton loading states while components stream
@@ -21,14 +35,11 @@
21
35
  * ```
22
36
  */
23
37
 
24
- import { Show, For, createSignal, onMount, onCleanup } from 'solid-js'
38
+ import { Show, For, createSignal, onMount } from 'solid-js'
25
39
  import { useStreamingUI, type UseStreamingUIOptions } from '../hooks/useStreamingUI'
26
40
  import type { UIComponent, RendererError } from '../types'
27
- import { validateComponent } from '../services/validation'
28
- import { GenerativeUIErrorBoundary } from './GenerativeUIErrorBoundary'
29
- import { markRenderStart, markRenderEnd, PERF_PREFIX } from '../utils/perf'
30
- import { useTelemetry } from '../context/MCPUITelemetryContext'
31
- import type { ValidationErrorMode } from './UIResourceRenderer'
41
+ import { UIResourceRenderer, type ValidationErrorMode } from './UIResourceRenderer'
42
+ import { useMCPUIStrings } from '../context/MCPUIStringsContext'
32
43
 
33
44
  export interface StreamingUIRendererProps extends UseStreamingUIOptions {
34
45
  class?: string
@@ -38,164 +49,27 @@ export interface StreamingUIRendererProps extends UseStreamingUIOptions {
38
49
  /**
39
50
  * How to react when a streamed component fails `validateComponent()`
40
51
  * (v5.4.0). Defaults to `'block'` (full red error card — pre-v5.4.0
41
- * behavior). See `ValidationErrorMode` in `UIResourceRenderer`.
52
+ * behavior). Forwarded to the delegated `<UIResourceRenderer>`.
42
53
  */
43
54
  errorMode?: ValidationErrorMode
55
+ /**
56
+ * Visibility behavior of the inline expand button on streamed components
57
+ * wrapped in `<ExpandableWrapper>` (v6.6.0 — parity with the static
58
+ * `<UIResourceRenderer toolbarVariant>` prop). Forwarded as-is.
59
+ */
60
+ toolbarVariant?: 'hover' | 'always-visible'
44
61
  }
45
62
 
46
63
  /**
47
- * Component Renderer - Inline lightweight version
48
- * (Full implementation in UIResourceRenderer)
64
+ * The 12-column placement of a streamed component is owned by this
65
+ * component's outer grid (the cell `<div>` below). Delegating the component
66
+ * verbatim to `<UIResourceRenderer>` would re-apply that placement inside a
67
+ * fresh nested 12-column grid and visually misplace it. We hand
68
+ * `<UIResourceRenderer>` a full-width copy so it only renders the component,
69
+ * not a competing layout.
49
70
  */
50
- function StreamingComponentRenderer(props: {
51
- component: UIComponent
52
- onError?: (error: RendererError) => void
53
- errorMode?: ValidationErrorMode
54
- }) {
55
- // Performance marks (v5.4.0) — see utils/perf.ts
56
- markRenderStart(props.component.id)
57
-
58
- // Telemetry sink (B.5 — v5.6.0). Same wiring as ComponentRenderer in
59
- // UIResourceRenderer.tsx — null when no Provider, no-op everywhere then.
60
- const telemetry = useTelemetry()
61
-
62
- onMount(() => {
63
- markRenderEnd(props.component.id)
64
- if (telemetry) {
65
- const ts = Date.now()
66
- telemetry.dispatch({
67
- type: 'component:mounted',
68
- id: props.component.id,
69
- componentType: props.component.type,
70
- ts,
71
- })
72
- if (typeof performance !== 'undefined' && typeof performance.getEntriesByName === 'function') {
73
- const entries = performance.getEntriesByName(`${PERF_PREFIX}${props.component.id}:render`, 'measure')
74
- const last = entries[entries.length - 1]
75
- if (last) {
76
- telemetry.dispatch({
77
- type: 'component:rendered',
78
- id: props.component.id,
79
- componentType: props.component.type,
80
- durationMs: last.duration,
81
- ts,
82
- })
83
- }
84
- }
85
- }
86
- })
87
-
88
- onCleanup(() => {
89
- if (telemetry) {
90
- telemetry.dispatch({
91
- type: 'component:unmounted',
92
- id: props.component.id,
93
- componentType: props.component.type,
94
- ts: Date.now(),
95
- })
96
- }
97
- })
98
-
99
- // Validate component before rendering
100
- const validation = validateComponent(props.component)
101
- if (!validation.valid) {
102
- props.onError?.({
103
- type: 'validation',
104
- message: 'Component validation failed',
105
- componentId: props.component.id,
106
- details: validation.errors,
107
- })
108
-
109
- if (telemetry) {
110
- telemetry.dispatch({
111
- type: 'validation:failed',
112
- id: props.component.id,
113
- componentType: props.component.type,
114
- errorCount: validation.errors?.length ?? 0,
115
- firstErrorCode: validation.errors?.[0]?.code ?? null,
116
- ts: Date.now(),
117
- })
118
- }
119
-
120
- const mode: ValidationErrorMode = props.errorMode ?? 'block'
121
- const firstError = validation.errors?.[0]?.message || 'Unknown validation error'
122
-
123
- if (mode === 'silent') {
124
- return null
125
- }
126
-
127
- if (mode === 'inline-warn') {
128
- return (
129
- <div
130
- class="inline-flex items-center gap-1.5 px-2 py-1 rounded-md bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 text-xs text-yellow-800 dark:text-yellow-200"
131
- role="alert"
132
- aria-label="Component validation warning"
133
- title={firstError}
134
- >
135
- <svg
136
- xmlns="http://www.w3.org/2000/svg"
137
- class="w-3.5 h-3.5"
138
- viewBox="0 0 24 24"
139
- fill="none"
140
- stroke="currentColor"
141
- stroke-width="2"
142
- stroke-linecap="round"
143
- stroke-linejoin="round"
144
- aria-hidden="true"
145
- >
146
- <path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
147
- <line x1="12" y1="9" x2="12" y2="13" />
148
- <line x1="12" y1="17" x2="12.01" y2="17" />
149
- </svg>
150
- <span>Invalid {props.component.type}</span>
151
- </div>
152
- )
153
- }
154
-
155
- return (
156
- <div class="w-full bg-error-subtle border border-border-error rounded-lg p-4">
157
- <p class="text-sm font-medium text-error-primary">Validation Error</p>
158
- <p class="text-xs text-text-secondary mt-1">
159
- {firstError}
160
- </p>
161
- </div>
162
- )
163
- }
164
-
165
- // Simplified renderer - just show component type and title
166
- // Full rendering logic in UIResourceRenderer
167
- const params = props.component.params as any
168
-
169
- return (
170
- <GenerativeUIErrorBoundary
171
- componentId={props.component.id}
172
- componentType={props.component.type}
173
- onError={props.onError}
174
- allowRetry={false}
175
- >
176
- <div class="w-full bg-surface-secondary border border-border-subtle rounded-lg p-4">
177
- <div class="flex items-center gap-2 mb-2">
178
- <span class="text-xs font-medium text-text-tertiary uppercase">
179
- {props.component.type}
180
- </span>
181
- </div>
182
- <Show when={params?.title}>
183
- <h3 class="text-sm font-semibold text-text-primary">{params.title}</h3>
184
- </Show>
185
- <Show when={props.component.type === 'metric' && params?.value}>
186
- <div class="mt-2">
187
- <p class="text-2xl font-semibold text-text-primary">{params.value}</p>
188
- <Show when={params.unit}>
189
- <span class="text-sm text-text-secondary">{params.unit}</span>
190
- </Show>
191
- </div>
192
- </Show>
193
- <div class="mt-3 text-xs text-text-tertiary">
194
- Component ID: {props.component.id.slice(0, 8)}...
195
- </div>
196
- </div>
197
- </GenerativeUIErrorBoundary>
198
- )
71
+ function asFullWidth(component: UIComponent): UIComponent {
72
+ return { ...component, position: { colStart: 1, colSpan: 12 } }
199
73
  }
200
74
 
201
75
  export function StreamingUIRenderer(props: StreamingUIRendererProps) {
@@ -210,6 +84,7 @@ export function StreamingUIRenderer(props: StreamingUIRendererProps) {
210
84
  onComponentReceived: props.onComponentReceived,
211
85
  })
212
86
 
87
+ const strings = useMCPUIStrings()
213
88
  const [animatingComponents, setAnimatingComponents] = createSignal<Set<string>>(new Set())
214
89
 
215
90
  // Track new components for animation
@@ -292,7 +167,7 @@ export function StreamingUIRenderer(props: StreamingUIRendererProps) {
292
167
  class="mt-3 rounded-md bg-error-primary px-3 py-1.5 text-sm font-medium text-white hover:bg-error-hover"
293
168
  onClick={() => startStreaming()}
294
169
  >
295
- Retry
170
+ {strings.retry}
296
171
  </button>
297
172
  </Show>
298
173
  </div>
@@ -300,7 +175,7 @@ export function StreamingUIRenderer(props: StreamingUIRendererProps) {
300
175
 
301
176
  {/* Components Grid */}
302
177
  <div class="grid grid-cols-12 gap-4">
303
- {/* Render received components */}
178
+ {/* Render received components — delegated to the real UIResourceRenderer */}
304
179
  <For each={components()}>
305
180
  {(component) => {
306
181
  // Trigger animation on mount (SSR-safe, no 'use' directive needed)
@@ -314,10 +189,11 @@ export function StreamingUIRenderer(props: StreamingUIRendererProps) {
314
189
  `}
315
190
  style={`grid-column-start: ${component.position.colStart}; grid-column-end: ${component.position.colStart + component.position.colSpan}`}
316
191
  >
317
- <StreamingComponentRenderer
318
- component={component}
319
- onError={props.onRenderError}
192
+ <UIResourceRenderer
193
+ content={asFullWidth(component)}
320
194
  errorMode={props.errorMode}
195
+ onError={props.onRenderError}
196
+ toolbarVariant={props.toolbarVariant}
321
197
  />
322
198
  </div>
323
199
  )
@@ -10,6 +10,16 @@ export type { UIResourceRendererProps } from './UIResourceRenderer'
10
10
  export { StreamingUIRenderer } from './StreamingUIRenderer'
11
11
  export type { StreamingUIRendererProps } from './StreamingUIRenderer'
12
12
 
13
+ // Presentation feedback (v6.6.0 — R3, distinct from FeedbackInline)
14
+ export {
15
+ PresentationFeedback,
16
+ DEFAULT_PRESENTATION_FEEDBACK_LABELS,
17
+ } from './PresentationFeedback'
18
+ export type {
19
+ PresentationFeedbackProps,
20
+ PresentationFeedbackLabels,
21
+ } from './PresentationFeedback'
22
+
13
23
  export { GenerativeUIErrorBoundary } from './GenerativeUIErrorBoundary'
14
24
  export type { GenerativeUIErrorBoundaryProps } from './GenerativeUIErrorBoundary'
15
25
 
@@ -0,0 +1,116 @@
1
+ /**
2
+ * v6.6.0 — MCPUIStringsProvider (D2 / R4 of ROADMAP-opendata-macro-mcpui).
3
+ *
4
+ * Coverage:
5
+ * 1. Defaults are English, available with no provider mounted
6
+ * 2. Provider does a partial merge over the EN defaults
7
+ * 3. FeedbackInline reads chrome strings from the provider
8
+ * 4. FeedbackInline `positiveAck` / `negativeAck` props still win over the provider
9
+ * 5. ExpandableWrapper reads the expand-button tooltip from the provider
10
+ */
11
+
12
+ import { describe, it, expect, beforeEach } from 'vitest'
13
+ import { render, cleanup, fireEvent } from '@solidjs/testing-library'
14
+ import {
15
+ MCPUIStringsProvider,
16
+ useMCPUIStrings,
17
+ DEFAULT_MCPUI_STRINGS,
18
+ } from './MCPUIStringsContext'
19
+ import { FeedbackInline } from '../components/FeedbackInline'
20
+ import { ExpandableWrapper } from '../components/ExpandableWrapper'
21
+
22
+ describe('MCPUIStringsContext (v6.6.0)', () => {
23
+ beforeEach(() => cleanup())
24
+
25
+ it('defaults are English', () => {
26
+ expect(DEFAULT_MCPUI_STRINGS.expand).toBe('Expand')
27
+ expect(DEFAULT_MCPUI_STRINGS.feedbackUseful).toBe('Useful')
28
+ expect(DEFAULT_MCPUI_STRINGS.feedbackPositiveAck).toBe('Thanks!')
29
+ expect(DEFAULT_MCPUI_STRINGS.retry).toBe('Retry')
30
+ })
31
+
32
+ it('useMCPUIStrings returns the EN defaults with no provider mounted', () => {
33
+ let captured: ReturnType<typeof useMCPUIStrings> | undefined
34
+ const Probe = () => {
35
+ captured = useMCPUIStrings()
36
+ return <span>probe</span>
37
+ }
38
+ render(() => <Probe />)
39
+ expect(captured).toEqual(DEFAULT_MCPUI_STRINGS)
40
+ })
41
+
42
+ it('provider partial-merges over the EN defaults', () => {
43
+ let captured: ReturnType<typeof useMCPUIStrings> | undefined
44
+ const Probe = () => {
45
+ captured = useMCPUIStrings()
46
+ return <span>probe</span>
47
+ }
48
+ render(() => (
49
+ <MCPUIStringsProvider strings={{ expand: 'Agrandir', feedbackUseful: 'Utile' }}>
50
+ <Probe />
51
+ </MCPUIStringsProvider>
52
+ ))
53
+ // Overridden keys
54
+ expect(captured!.expand).toBe('Agrandir')
55
+ expect(captured!.feedbackUseful).toBe('Utile')
56
+ // Untouched keys fall back to EN
57
+ expect(captured!.retry).toBe('Retry')
58
+ expect(captured!.closeExpandedView).toBe('Close expanded view')
59
+ })
60
+
61
+ it('FeedbackInline reads its ack from the provider (FR override)', () => {
62
+ const { getByText, container } = render(() => (
63
+ <MCPUIStringsProvider
64
+ strings={{ feedbackPositiveAck: 'Merci !', feedbackUseful: 'Utile' }}
65
+ >
66
+ <FeedbackInline onSubmit={() => {}} />
67
+ </MCPUIStringsProvider>
68
+ ))
69
+ // Tooltip from provider
70
+ const upBtn = container.querySelector('[data-feedback-inline-rating="positive"]')
71
+ expect(upBtn?.getAttribute('title')).toBe('Utile')
72
+ // Ack from provider after click
73
+ fireEvent.click(upBtn!)
74
+ expect(getByText('Merci !')).toBeTruthy()
75
+ })
76
+
77
+ it('FeedbackInline defaults to EN ack when no provider is mounted', () => {
78
+ const { getByText, container } = render(() => <FeedbackInline onSubmit={() => {}} />)
79
+ const upBtn = container.querySelector('[data-feedback-inline-rating="positive"]')
80
+ fireEvent.click(upBtn!)
81
+ expect(getByText('Thanks!')).toBeTruthy()
82
+ })
83
+
84
+ it('FeedbackInline positiveAck prop still wins over the provider', () => {
85
+ const { getByText, container } = render(() => (
86
+ <MCPUIStringsProvider strings={{ feedbackPositiveAck: 'FromProvider' }}>
87
+ <FeedbackInline onSubmit={() => {}} positiveAck="FromProp" />
88
+ </MCPUIStringsProvider>
89
+ ))
90
+ const upBtn = container.querySelector('[data-feedback-inline-rating="positive"]')
91
+ fireEvent.click(upBtn!)
92
+ expect(getByText('FromProp')).toBeTruthy()
93
+ })
94
+
95
+ it('ExpandableWrapper reads the expand-button tooltip from the provider', () => {
96
+ const { container } = render(() => (
97
+ <MCPUIStringsProvider strings={{ expand: 'Plein écran' }}>
98
+ <ExpandableWrapper title="Données">
99
+ <div>content</div>
100
+ </ExpandableWrapper>
101
+ </MCPUIStringsProvider>
102
+ ))
103
+ const expandBtn = container.querySelector('button[aria-label="Expand to fullscreen"]')
104
+ expect(expandBtn?.getAttribute('title')).toBe('Plein écran')
105
+ })
106
+
107
+ it('ExpandableWrapper falls back to EN with no provider', () => {
108
+ const { container } = render(() => (
109
+ <ExpandableWrapper title="Data">
110
+ <div>content</div>
111
+ </ExpandableWrapper>
112
+ ))
113
+ const expandBtn = container.querySelector('button[aria-label="Expand to fullscreen"]')
114
+ expect(expandBtn?.getAttribute('title')).toBe('Expand')
115
+ })
116
+ })
@@ -0,0 +1,128 @@
1
+ /**
2
+ * MCPUIStringsContext — i18n for the library's own "chrome" strings.
3
+ *
4
+ * @since v6.6.0 (D2 / R4 of ROADMAP-opendata-macro-mcpui)
5
+ *
6
+ * ## Scope — chrome only, NOT content
7
+ *
8
+ * MCP-UI renders two kinds of text :
9
+ *
10
+ * - **Content** — table headers, chart titles, action labels, prompt
11
+ * questions. These come from the payload and are ALREADY localized by
12
+ * whoever produced the payload (the connector / MCP server). MCP-UI
13
+ * renders them verbatim and this context never touches them.
14
+ * - **Chrome** — the handful of strings the library itself hardcodes :
15
+ * the expand-button tooltip, the feedback acknowledgements, etc. THIS
16
+ * is what `MCPUIStrings` covers.
17
+ *
18
+ * There is deliberately no full i18n framework here : no per-renderer
19
+ * `locale` prop, no message catalogue, no ICU. A flat string map behind a
20
+ * context is enough — the chrome surface is small and static.
21
+ *
22
+ * ## Defaults are English
23
+ *
24
+ * `DEFAULT_MCPUI_STRINGS` is English. A published library should not ship
25
+ * hardcoded French. Consumers that want another language wrap their tree
26
+ * in `<MCPUIStringsProvider strings={...}>` with a partial override.
27
+ *
28
+ * Component props that already carry a label (e.g. `FeedbackInline`'s
29
+ * `positiveAck`, `ExpandableWrapper`'s `copyLabel`) keep priority over the
30
+ * provider — the provider only fills the gap when no explicit prop is set.
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * import { MCPUIStringsProvider } from '@seed-ship/mcp-ui-solid'
35
+ *
36
+ * <MCPUIStringsProvider strings={{ expand: 'Agrandir', feedbackUseful: 'Utile' }}>
37
+ * <App />
38
+ * </MCPUIStringsProvider>
39
+ * ```
40
+ */
41
+
42
+ import { createContext, useContext, type JSX } from 'solid-js'
43
+
44
+ /**
45
+ * The library's own chrome strings. Flat map, no interpolation.
46
+ *
47
+ * Marked exhaustive as of v6.6.0 ; new chrome strings added by later
48
+ * renderers extend this interface (the `MCPUIStringsProvider` merge keeps
49
+ * older consumers working — any unset key falls back to the EN default).
50
+ */
51
+ export interface MCPUIStrings {
52
+ // ── ExpandableWrapper toolbar ──────────────────────────────
53
+ /** `title` of the expand-to-fullscreen button. */
54
+ expand: string
55
+ /** Heading + `aria-label` of the fullscreen modal when no title is given. */
56
+ expandedView: string
57
+ /** Default tooltip of the copy button (overridden by `copyLabel` prop). */
58
+ copyToClipboard: string
59
+ /** `aria-label` of the close button in the fullscreen modal. */
60
+ closeExpandedView: string
61
+
62
+ // ── FeedbackInline (response-quality feedback) ─────────────
63
+ /** `title` of the thumb-up button. */
64
+ feedbackUseful: string
65
+ /** `title` of the thumb-down button. */
66
+ feedbackNotUseful: string
67
+ /** Acknowledgement shown after a positive rating (overridden by `positiveAck`). */
68
+ feedbackPositiveAck: string
69
+ /** Acknowledgement shown after a negative rating (overridden by `negativeAck`). */
70
+ feedbackNegativeAck: string
71
+
72
+ // ── Generic chrome ────────────────────────────────────────
73
+ /** Label of the streaming retry button. */
74
+ retry: string
75
+ }
76
+
77
+ /**
78
+ * English defaults. A published library ships no hardcoded non-English
79
+ * chrome — consumers localize via `<MCPUIStringsProvider>`.
80
+ */
81
+ export const DEFAULT_MCPUI_STRINGS: MCPUIStrings = {
82
+ expand: 'Expand',
83
+ expandedView: 'Expanded view',
84
+ copyToClipboard: 'Copy to clipboard',
85
+ closeExpandedView: 'Close expanded view',
86
+ feedbackUseful: 'Useful',
87
+ feedbackNotUseful: 'Not useful',
88
+ feedbackPositiveAck: 'Thanks!',
89
+ feedbackNegativeAck: "Noted — we'll improve",
90
+ retry: 'Retry',
91
+ }
92
+
93
+ export const MCPUIStringsContext = createContext<MCPUIStrings>(DEFAULT_MCPUI_STRINGS)
94
+
95
+ /**
96
+ * Reads the active chrome strings. Returns `DEFAULT_MCPUI_STRINGS` when no
97
+ * `<MCPUIStringsProvider>` is mounted above — every renderer works
98
+ * standalone with English chrome.
99
+ */
100
+ export function useMCPUIStrings(): MCPUIStrings {
101
+ return useContext(MCPUIStringsContext)
102
+ }
103
+
104
+ export interface MCPUIStringsProviderProps {
105
+ /**
106
+ * Partial override of the chrome strings. Any key left unset falls back
107
+ * to the English `DEFAULT_MCPUI_STRINGS` — so a consumer can localize
108
+ * just the strings they care about.
109
+ */
110
+ strings?: Partial<MCPUIStrings>
111
+ children: JSX.Element
112
+ }
113
+
114
+ /**
115
+ * Provides localized chrome strings to every MCP-UI renderer below it.
116
+ * Merges the partial `strings` override over the English defaults.
117
+ */
118
+ export function MCPUIStringsProvider(props: MCPUIStringsProviderProps): JSX.Element {
119
+ // `props.strings` is read inside the getter so a reactive override
120
+ // (signal-backed) re-propagates ; for the common static case it is read
121
+ // once at mount.
122
+ const value = (): MCPUIStrings => ({ ...DEFAULT_MCPUI_STRINGS, ...props.strings })
123
+ return (
124
+ <MCPUIStringsContext.Provider value={value()}>
125
+ {props.children}
126
+ </MCPUIStringsContext.Provider>
127
+ )
128
+ }
package/src/index.ts CHANGED
@@ -40,6 +40,21 @@ export { ComponentToolbar } from './components/ComponentToolbar'
40
40
  export { FeedbackInline } from './components/FeedbackInline'
41
41
  export type { FeedbackInlineProps, FeedbackInlineContext } from './components/FeedbackInline'
42
42
 
43
+ // Presentation feedback (v6.6.0 — R3 ; separate axis from FeedbackInline)
44
+ export {
45
+ PresentationFeedback,
46
+ DEFAULT_PRESENTATION_FEEDBACK_LABELS,
47
+ } from './components/PresentationFeedback'
48
+ export type {
49
+ PresentationFeedbackProps,
50
+ PresentationFeedbackLabels,
51
+ } from './components/PresentationFeedback'
52
+ export type {
53
+ ConnectorRenderFeedback,
54
+ ConnectorRenderProblem,
55
+ ConnectorPreferredLayout,
56
+ } from '@seed-ship/mcp-ui-spec'
57
+
43
58
  // Chat Bus (v2.4.0 — @experimental)
44
59
  export { ChatBusProvider, useChatBus } from './hooks/useChatBus'
45
60
  export { ChatPrompt } from './components/ChatPrompt'
@@ -113,6 +128,18 @@ export type {
113
128
  DuplicateMountReporter,
114
129
  } from './utils/duplicate-mount-registry'
115
130
 
131
+ // Chrome i18n — library's own strings, EN defaults (v6.6.0 — D2/R4)
132
+ export {
133
+ MCPUIStringsProvider,
134
+ MCPUIStringsContext,
135
+ useMCPUIStrings,
136
+ DEFAULT_MCPUI_STRINGS,
137
+ } from './context/MCPUIStringsContext'
138
+ export type {
139
+ MCPUIStrings,
140
+ MCPUIStringsProviderProps,
141
+ } from './context/MCPUIStringsContext'
142
+
116
143
  // Telemetry sink (B.5 — v5.6.0)
117
144
  export {
118
145
  MCPUITelemetryProvider,