@softize/opus 25.7.0 → 26.1.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 (41) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/bin/lib/check.mjs +9 -32
  3. package/bin/lib/copy.mjs +7 -4
  4. package/docs/adr/0014-structural-headers-do-not-carry-description.md +1 -1
  5. package/docs/adr/0018-modal-header-only-names-the-surface.md +1 -1
  6. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +4 -0
  7. package/docs/adr/0058-mensagem-da-ia-aceita-imagem-e-o-chat-recebe-anexos.md +201 -0
  8. package/docs/adr/0059-subheadings-orient-without-carrying-business-rules.md +51 -0
  9. package/docs/adr/README.md +2 -0
  10. package/package.json +1 -1
  11. package/registry/skills/build-opus-ui/references/evaluations.md +8 -0
  12. package/registry/skills/build-opus-ui/references/ui-patterns.md +14 -4
  13. package/src/ai/drivers/anthropic.ts +49 -9
  14. package/src/ai/index.ts +5 -2
  15. package/src/core/ai-message.ts +20 -0
  16. package/src/core/index.ts +5 -0
  17. package/src/core/types.ts +28 -4
  18. package/src/ui/components/patterns/action-list-dialog.tsx +14 -7
  19. package/src/ui/components/patterns/form-dialog.tsx +18 -7
  20. package/src/ui/components/patterns/form.tsx +36 -8
  21. package/src/ui/components/patterns/page.tsx +22 -0
  22. package/src/ui/components/patterns/surface-anatomy.ts +1 -1
  23. package/src/ui/components/primitives/chat.tsx +179 -22
  24. package/src/ui/components/primitives/composer.tsx +397 -7
  25. package/src/ui/components/primitives/dialog.tsx +28 -1
  26. package/src/ui/components/primitives/drawer.tsx +30 -2
  27. package/src/ui/docs/content/action-form-card.md +1 -1
  28. package/src/ui/docs/content/action-form-dialog.md +8 -0
  29. package/src/ui/docs/content/action-form.md +18 -5
  30. package/src/ui/docs/content/action-list-dialog.md +8 -0
  31. package/src/ui/docs/content/ai.md +46 -0
  32. package/src/ui/docs/content/chat.md +97 -2
  33. package/src/ui/docs/content/composer.md +144 -1
  34. package/src/ui/docs/content/dialog.md +24 -2
  35. package/src/ui/docs/content/drawer.md +24 -2
  36. package/src/ui/docs/content/page.md +23 -1
  37. package/src/ui/docs/content/surface.md +8 -2
  38. package/src/ui/lib/modal-layers.ts +27 -3
  39. package/src/ui/lib/prepare-image.ts +386 -0
  40. package/src/ui/meta.ts +2 -2
  41. package/src/ui/react.tsx +21 -1
@@ -0,0 +1,386 @@
1
+ /**
2
+ * Preparação de imagem no navegador antes do envio (ADR 0058, seção 6).
3
+ *
4
+ * Lê só o cabeçalho para conferir formato, animação e dimensões — e recusar, antes de decodificar,
5
+ * a imagem grande demais para a memória do navegador —, reduz o lado maior respeitando a orientação
6
+ * gravada pela câmera e codifica de novo sempre, o que também descarta metadados como a localização.
7
+ * Os tetos vêm do app; os padrões repetem os da ADR 0122 da Grand Brasil, primeiro consumidor.
8
+ */
9
+
10
+ import type { AiImageMediaType } from '../../core/index.ts'
11
+
12
+ /** Os formatos que a mensagem da IA aceita, na ordem em que são apresentados à pessoa. */
13
+ export const IMAGE_MEDIA_TYPES: readonly AiImageMediaType[] = ['image/png', 'image/jpeg', 'image/webp']
14
+
15
+ /** Padrões de `prepareImage`. O app deve passar os próprios tetos: o utilitário não decide teto
16
+ * de produto. */
17
+ export const PREPARE_IMAGE_DEFAULTS = {
18
+ /** Lado maior, em pixels, depois da redução (o limite de lado do nível padrão do Claude). */
19
+ maxEdge: 1568,
20
+ /** Teto por imagem depois da redução, em bytes (3 MB). */
21
+ maxBytes: 3 * 1024 * 1024,
22
+ /** Qualidade da codificação JPEG ou WebP. */
23
+ quality: 0.85,
24
+ /** Teto de pixels da imagem original (40 megapixels), conferido pelo cabeçalho antes de
25
+ * decodificar: uma foto muito grande ocupa centenas de megabytes de memória ao abrir. */
26
+ maxPixels: 40_000_000,
27
+ } as const
28
+
29
+ export interface PrepareImageOptions {
30
+ /** Lado maior, em pixels, depois da redução. Padrão: 1568. */
31
+ maxEdge?: number
32
+ /** Teto em bytes da imagem preparada. Padrão: 3 MB. */
33
+ maxBytes?: number
34
+ /** Formatos aceitos na entrada. Padrão: PNG, JPEG e WebP. */
35
+ accept?: readonly AiImageMediaType[]
36
+ /** Formato da codificação com perdas, usado para JPEG, WebP e o PNG que passa do teto.
37
+ * Padrão: `image/jpeg`. */
38
+ lossyType?: 'image/jpeg' | 'image/webp'
39
+ /** Qualidade da codificação com perdas, de 0 a 1. Padrão: 0,85. */
40
+ quality?: number
41
+ /** Teto de pixels (largura × altura) da imagem original, lido do cabeçalho antes de
42
+ * decodificar. Padrão: 40 megapixels. */
43
+ maxPixels?: number
44
+ }
45
+
46
+ export type PrepareImageRejection =
47
+ | 'unsupported-type'
48
+ | 'animated'
49
+ | 'too-many-pixels'
50
+ | 'too-large'
51
+ | 'unreadable'
52
+
53
+ export interface PreparedImage {
54
+ ok: true
55
+ /** A imagem preparada, com o nome ajustado à extensão do formato final. */
56
+ file: File
57
+ mediaType: AiImageMediaType
58
+ width: number
59
+ height: number
60
+ /** Tamanho em bytes. */
61
+ size: number
62
+ /** A imagem foi reduzida para caber em `maxEdge`. */
63
+ resized: boolean
64
+ }
65
+
66
+ export interface RejectedImage {
67
+ ok: false
68
+ reason: PrepareImageRejection
69
+ /** Explicação em pt-BR para a pessoa, pronta para a interface. */
70
+ message: string
71
+ }
72
+
73
+ export type PrepareImageResult = PreparedImage | RejectedImage
74
+
75
+ const TYPE_LABELS: Record<string, string> = {
76
+ 'image/png': 'PNG',
77
+ 'image/jpeg': 'JPEG',
78
+ 'image/webp': 'WebP',
79
+ 'image/gif': 'GIF',
80
+ }
81
+
82
+ const EXTENSIONS: Record<AiImageMediaType, string> = {
83
+ 'image/png': '.png',
84
+ 'image/jpeg': '.jpg',
85
+ 'image/webp': '.webp',
86
+ }
87
+
88
+ /** "PNG, JPEG ou WebP" — a lista de formatos aceitos como a pessoa a lê. */
89
+ export function imageTypeList(types: readonly string[]): string {
90
+ const labels = types.map((type) => TYPE_LABELS[type] ?? type.replace(/^image\//, '').toUpperCase())
91
+ if (labels.length <= 1) return labels.join('')
92
+ return `${labels.slice(0, -1).join(', ')} ou ${labels[labels.length - 1]}`
93
+ }
94
+
95
+ /** "3 MB", "1,5 MB" — tamanho em megabytes como a pessoa o lê. */
96
+ export function formatMegabytes(bytes: number): string {
97
+ const megabytes = bytes / (1024 * 1024)
98
+ return `${megabytes.toLocaleString('pt-BR', { maximumFractionDigits: 1 })} MB`
99
+ }
100
+
101
+ /** Quanto do início do arquivo é lido de uma vez. Basta para o cabeçalho de quase toda imagem;
102
+ * o que estiver além (um bloco de metadados grande antes do quadro) é lido por partes. */
103
+ const HEAD_BYTES = 64 * 1024
104
+
105
+ /** Leitura por faixa, sem carregar o arquivo inteiro: o início fica em memória, o resto vem por
106
+ * `slice` só quando a varredura dos blocos chega lá. */
107
+ function byteReader(file: Blob, head: Uint8Array): (offset: number, length: number) => Promise<Uint8Array> {
108
+ return async (offset, length) => {
109
+ if (offset + length <= head.length) return head.subarray(offset, offset + length)
110
+ return new Uint8Array(await file.slice(offset, offset + length).arrayBuffer())
111
+ }
112
+ }
113
+
114
+ const ascii = (bytes: Uint8Array, start: number, end: number): string =>
115
+ String.fromCharCode(...bytes.subarray(start, end))
116
+ const uint16be = (b: Uint8Array, i: number): number => (b[i] << 8) | b[i + 1]
117
+ const uint32be = (b: Uint8Array, i: number): number => ((b[i] << 24) | (b[i + 1] << 16) | (b[i + 2] << 8) | b[i + 3]) >>> 0
118
+ const uint24le = (b: Uint8Array, i: number): number => b[i] | (b[i + 1] << 8) | (b[i + 2] << 16)
119
+
120
+ interface ImageHeader {
121
+ /** Formato pelos primeiros bytes; `null` quando não é um formato conhecido. */
122
+ type: string | null
123
+ /** Dimensões gravadas no cabeçalho, antes de aplicar a orientação; ausentes quando ilegíveis. */
124
+ width?: number
125
+ height?: number
126
+ animated: boolean
127
+ }
128
+
129
+ /** O formato real da imagem, pelos primeiros bytes. */
130
+ function sniffType(bytes: Uint8Array): string | null {
131
+ if (bytes.length >= 8 && bytes[0] === 0x89 && ascii(bytes, 1, 4) === 'PNG') return 'image/png'
132
+ if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return 'image/jpeg'
133
+ if (bytes.length >= 12 && ascii(bytes, 0, 4) === 'RIFF' && ascii(bytes, 8, 12) === 'WEBP') return 'image/webp'
134
+ if (bytes.length >= 4 && ascii(bytes, 0, 4) === 'GIF8') return 'image/gif'
135
+ return null
136
+ }
137
+
138
+ /** PNG: dimensões no `IHDR`, o primeiro bloco; animação (APNG) é o bloco `acTL` antes do `IDAT`. */
139
+ async function inspectPng(head: Uint8Array, read: ReturnType<typeof byteReader>, size: number): Promise<ImageHeader> {
140
+ if (head.length < 24 || ascii(head, 12, 16) !== 'IHDR') return { type: 'image/png', animated: false }
141
+ const header: ImageHeader = { type: 'image/png', width: uint32be(head, 16), height: uint32be(head, 20), animated: false }
142
+ for (let offset = 8; offset + 8 <= size; ) {
143
+ const chunk = await read(offset, 8)
144
+ if (chunk.length < 8) break
145
+ const type = ascii(chunk, 4, 8)
146
+ if (type === 'acTL') return { ...header, animated: true }
147
+ if (type === 'IDAT' || type === 'IEND') break
148
+ offset += 12 + uint32be(chunk, 0)
149
+ }
150
+ return header
151
+ }
152
+
153
+ /** JPEG: dimensões no primeiro marcador de quadro (SOF0 a SOF15, sem DHT, JPG e DAC), depois dos
154
+ * segmentos de aplicação, como o EXIF. */
155
+ async function inspectJpeg(read: ReturnType<typeof byteReader>, size: number): Promise<ImageHeader> {
156
+ let offset = 2
157
+ for (let guard = 0; guard < 4096 && offset + 4 <= size; guard++) {
158
+ const segment = await read(offset, 9)
159
+ if (segment[0] !== 0xff) break
160
+ const marker = segment[1]
161
+ if (marker === 0xff) {
162
+ offset += 1 // Bytes de preenchimento antes do marcador.
163
+ continue
164
+ }
165
+ if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd8)) {
166
+ offset += 2
167
+ continue
168
+ }
169
+ if (marker === 0xd9 || marker === 0xda) break // Fim, ou dados da imagem sem quadro declarado.
170
+ if (segment.length < 4) break
171
+ const isFrame = marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc
172
+ if (isFrame && segment.length >= 9) {
173
+ return { type: 'image/jpeg', height: uint16be(segment, 5), width: uint16be(segment, 7), animated: false }
174
+ }
175
+ const length = uint16be(segment, 2)
176
+ if (length < 2) break
177
+ offset += 2 + length
178
+ }
179
+ return { type: 'image/jpeg', animated: false }
180
+ }
181
+
182
+ /** WebP: dimensões no primeiro bloco (`VP8 `, `VP8L` ou `VP8X`); animação é o bit do `VP8X`. */
183
+ function inspectWebp(head: Uint8Array): ImageHeader {
184
+ const chunk = head.length >= 16 ? ascii(head, 12, 16) : ''
185
+ if (chunk === 'VP8X' && head.length >= 30) {
186
+ return {
187
+ type: 'image/webp',
188
+ width: 1 + uint24le(head, 24),
189
+ height: 1 + uint24le(head, 27),
190
+ animated: (head[20] & 0x02) !== 0,
191
+ }
192
+ }
193
+ if (chunk === 'VP8 ' && head.length >= 30) {
194
+ return {
195
+ type: 'image/webp',
196
+ width: (head[26] | (head[27] << 8)) & 0x3fff,
197
+ height: (head[28] | (head[29] << 8)) & 0x3fff,
198
+ animated: false,
199
+ }
200
+ }
201
+ if (chunk === 'VP8L' && head.length >= 25 && head[20] === 0x2f) {
202
+ return {
203
+ type: 'image/webp',
204
+ width: 1 + (head[21] | ((head[22] & 0x3f) << 8)),
205
+ height: 1 + ((head[22] >> 6) | (head[23] << 2) | ((head[24] & 0x0f) << 10)),
206
+ animated: false,
207
+ }
208
+ }
209
+ return { type: 'image/webp', animated: false }
210
+ }
211
+
212
+ async function inspectHeader(file: Blob): Promise<ImageHeader> {
213
+ const head = new Uint8Array(await file.slice(0, HEAD_BYTES).arrayBuffer())
214
+ const type = sniffType(head)
215
+ const read = byteReader(file, head)
216
+ if (type === 'image/png') return inspectPng(head, read, file.size)
217
+ if (type === 'image/jpeg') return inspectJpeg(read, file.size)
218
+ if (type === 'image/webp') return inspectWebp(head)
219
+ return { type, animated: false }
220
+ }
221
+
222
+ /** "48 megapixels", "40,5 megapixels". */
223
+ function formatMegapixels(pixels: number): string {
224
+ return `${(pixels / 1_000_000).toLocaleString('pt-BR', { maximumFractionDigits: 1 })} megapixels`
225
+ }
226
+
227
+ function reject(reason: PrepareImageRejection, message: string): RejectedImage {
228
+ return { ok: false, reason, message }
229
+ }
230
+
231
+ interface DecodedImage {
232
+ source: CanvasImageSource
233
+ width: number
234
+ height: number
235
+ release: () => void
236
+ }
237
+
238
+ /** Decodifica aplicando a orientação gravada pela câmera (EXIF): a foto tirada com o celular em
239
+ * pé sai em pé, com largura e altura já trocadas. */
240
+ async function decode(file: Blob): Promise<DecodedImage> {
241
+ if (typeof globalThis.createImageBitmap === 'function') {
242
+ let bitmap: ImageBitmap
243
+ try {
244
+ bitmap = await globalThis.createImageBitmap(file, { imageOrientation: 'from-image' })
245
+ } catch (error) {
246
+ // Navegador anterior ao valor `from-image` recusa a opção com TypeError; o padrão dele
247
+ // também aplica a orientação do arquivo.
248
+ if (!(error instanceof TypeError)) throw error
249
+ bitmap = await globalThis.createImageBitmap(file)
250
+ }
251
+ return { source: bitmap, width: bitmap.width, height: bitmap.height, release: () => bitmap.close() }
252
+ }
253
+ const url = URL.createObjectURL(file)
254
+ try {
255
+ const image = new Image()
256
+ // `from-image` é o padrão do CSS: dimensões naturais e `drawImage` seguem a orientação.
257
+ image.style.imageOrientation = 'from-image'
258
+ image.src = url
259
+ await image.decode()
260
+ return { source: image, width: image.naturalWidth, height: image.naturalHeight, release: () => {} }
261
+ } finally {
262
+ URL.revokeObjectURL(url)
263
+ }
264
+ }
265
+
266
+ type Encoder = (type: AiImageMediaType, quality: number) => Promise<Blob>
267
+
268
+ /** Prepara o quadro na dimensão final e devolve o codificador. O desenho acontece na primeira
269
+ * codificação e só se repete quando o fundo muda: JPEG não tem transparência e, sem fundo, o
270
+ * transparente viraria preto. */
271
+ function canvasFor(image: DecodedImage, width: number, height: number): Encoder {
272
+ const canvas: OffscreenCanvas | HTMLCanvasElement =
273
+ typeof globalThis.OffscreenCanvas === 'function'
274
+ ? new OffscreenCanvas(width, height)
275
+ : Object.assign(document.createElement('canvas'), { width, height })
276
+ const context = canvas.getContext('2d') as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null
277
+ if (context === null) throw new Error('canvas 2d indisponível')
278
+ let painted: 'transparent' | 'white' | null = null
279
+ const paint = (background: 'transparent' | 'white'): void => {
280
+ if (painted === background) return
281
+ context.clearRect(0, 0, width, height)
282
+ if (background === 'white') {
283
+ context.fillStyle = '#ffffff'
284
+ context.fillRect(0, 0, width, height)
285
+ }
286
+ context.imageSmoothingQuality = 'high'
287
+ context.drawImage(image.source, 0, 0, width, height)
288
+ painted = background
289
+ }
290
+ return async (type, quality) => {
291
+ paint(type === 'image/jpeg' ? 'white' : 'transparent')
292
+ const blob =
293
+ 'convertToBlob' in canvas
294
+ ? await canvas.convertToBlob({ type, quality })
295
+ : await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, type, quality))
296
+ // Um navegador que não codifica o formato pedido devolve PNG em silêncio.
297
+ if (blob === null || blob.type !== type) throw new Error(`codificação ${type} indisponível`)
298
+ return blob
299
+ }
300
+ }
301
+
302
+ function renamed(name: string, type: AiImageMediaType): string {
303
+ const base = name.replace(/\.[^./\\]+$/, '') || 'imagem'
304
+ return `${base}${EXTENSIONS[type]}`
305
+ }
306
+
307
+ /**
308
+ * Prepara uma imagem para ir à conversa: confere o formato pelos bytes, recusa animação, reduz o
309
+ * lado maior até `maxEdge` e codifica de novo. PNG continua PNG quando cabe em `maxBytes`; o resto
310
+ * vira `lossyType` (JPEG por padrão) com `quality`. O que continua acima do teto é recusado.
311
+ *
312
+ * Nunca lança: uma falha vira `{ ok: false, reason, message }`, com a mensagem pronta para a pessoa.
313
+ */
314
+ export async function prepareImage(file: Blob & { name?: string }, options: PrepareImageOptions = {}): Promise<PrepareImageResult> {
315
+ const maxEdge = options.maxEdge ?? PREPARE_IMAGE_DEFAULTS.maxEdge
316
+ const maxBytes = options.maxBytes ?? PREPARE_IMAGE_DEFAULTS.maxBytes
317
+ const accept = options.accept ?? IMAGE_MEDIA_TYPES
318
+ const lossyType = options.lossyType ?? 'image/jpeg'
319
+ const quality = options.quality ?? PREPARE_IMAGE_DEFAULTS.quality
320
+ const maxPixels = options.maxPixels ?? PREPARE_IMAGE_DEFAULTS.maxPixels
321
+ const unreadable = (): RejectedImage =>
322
+ reject('unreadable', 'Não foi possível abrir esta imagem. Confira o arquivo ou tente outra imagem.')
323
+
324
+ let header: ImageHeader
325
+ try {
326
+ header = await inspectHeader(file)
327
+ } catch {
328
+ return unreadable()
329
+ }
330
+
331
+ const sniffed = header.type
332
+ if (sniffed === null || !(accept as readonly string[]).includes(sniffed)) {
333
+ const found = sniffed !== null ? `Esta imagem está em ${TYPE_LABELS[sniffed] ?? sniffed}, formato não aceito. ` : ''
334
+ return reject('unsupported-type', `${found}Envie uma imagem ${imageTypeList(accept)}.`)
335
+ }
336
+ const sourceType = sniffed as AiImageMediaType
337
+ if (header.animated) {
338
+ return reject('animated', 'Imagens animadas não são aceitas. Envie uma imagem parada, como um print da tela.')
339
+ }
340
+ if (header.width === undefined || header.height === undefined || header.width <= 0 || header.height <= 0) {
341
+ return unreadable()
342
+ }
343
+ const pixels = header.width * header.height
344
+ if (pixels > maxPixels) {
345
+ return reject(
346
+ 'too-many-pixels',
347
+ `A imagem tem ${formatMegapixels(pixels)}, acima do limite de ${formatMegapixels(maxPixels)}. ` +
348
+ 'Reduza a imagem ou envie um recorte antes de anexar.',
349
+ )
350
+ }
351
+
352
+ let image: DecodedImage
353
+ try {
354
+ image = await decode(file)
355
+ } catch {
356
+ return unreadable()
357
+ }
358
+
359
+ try {
360
+ if (image.width <= 0 || image.height <= 0) return unreadable()
361
+ const scale = Math.min(1, maxEdge / Math.max(image.width, image.height))
362
+ const width = Math.max(1, Math.round(image.width * scale))
363
+ const height = Math.max(1, Math.round(image.height * scale))
364
+ const encode = canvasFor(image, width, height)
365
+
366
+ // PNG continua PNG quando cabe no teto; o resto, e o PNG que passa dele, vira `lossyType`.
367
+ let mediaType: AiImageMediaType = 'image/png'
368
+ let blob = sourceType === 'image/png' ? await encode('image/png', 1) : null
369
+ if (blob === null || blob.size > maxBytes) {
370
+ mediaType = lossyType
371
+ blob = await encode(mediaType, quality)
372
+ }
373
+ if (blob.size > maxBytes) {
374
+ return reject(
375
+ 'too-large',
376
+ `A imagem continua com mais de ${formatMegabytes(maxBytes)} depois de reduzida. Recorte a imagem ou envie uma versão menor.`,
377
+ )
378
+ }
379
+ const prepared = new File([blob], renamed(file.name ?? 'imagem', mediaType), { type: mediaType })
380
+ return { ok: true, file: prepared, mediaType, width, height, size: prepared.size, resized: scale < 1 }
381
+ } catch {
382
+ return unreadable()
383
+ } finally {
384
+ image.release()
385
+ }
386
+ }
package/src/ui/meta.ts CHANGED
@@ -87,7 +87,7 @@ export const componentMeta = {
87
87
  name: "chat",
88
88
  ancestry: "opus",
89
89
  whenToUse:
90
- "Monte uma conversa completa com mensagens, composição, carregamento e rolagem automática. A propriedade `send` aceita resposta integral ou streaming de eventos; use Composer quando precisar apenas da entrada de texto. Defina uma altura para o contêiner.",
90
+ "Monte uma conversa completa com mensagens, composição, carregamento e rolagem automática. A propriedade `send` aceita resposta integral ou streaming de eventos; no modo controlado, `composerAttachments` permite anexar imagens e o transcript mostra as que o aplicativo guardou. Use Composer quando precisar apenas da entrada de texto. Defina uma altura para o contêiner.",
91
91
  },
92
92
  checkbox: {
93
93
  name: "checkbox",
@@ -111,7 +111,7 @@ export const componentMeta = {
111
111
  name: "composer",
112
112
  ancestry: "opus",
113
113
  whenToUse:
114
- "Receba texto para envio sem montar uma conversa completa. Enter envia, Shift+Enter quebra a linha e `actions` acrescenta uma faixa para seletores relacionados. O componente é controlado; use Chat quando também precisar de mensagens e estado da conversa.",
114
+ "Receba texto para envio sem montar uma conversa completa. Enter envia, Shift+Enter quebra a linha e `actions` acrescenta uma faixa para seletores relacionados. `attachments` permite anexar imagens pelo botão, colando ou soltando, com o estado mantido pelo aplicativo; `prepareImage` reduz a imagem antes do envio. O componente é controlado; use Chat quando também precisar de mensagens e estado da conversa.",
115
115
  },
116
116
  copyable: {
117
117
  name: "copyable",
package/src/ui/react.tsx CHANGED
@@ -59,13 +59,30 @@ export type {
59
59
  ChatProps,
60
60
  ChatMessage,
61
61
  ChatArtifact,
62
+ ChatAttachment,
63
+ ChatAttachmentStatus,
62
64
  ChatTranscriptItem,
63
65
  ChatTranscriptMessage,
64
66
  } from "./components/primitives/chat.tsx";
65
67
  export { Ask } from "./components/primitives/ask.tsx";
66
68
  export type { AskProps } from "./components/primitives/ask.tsx";
67
69
  export { Composer } from "./components/primitives/composer.tsx";
68
- export type { ComposerProps } from "./components/primitives/composer.tsx";
70
+ export type {
71
+ ComposerProps,
72
+ ComposerAttachment,
73
+ ComposerAttachmentStatus,
74
+ ComposerAttachments,
75
+ ComposerFileRejection,
76
+ } from "./components/primitives/composer.tsx";
77
+ // Preparação da imagem no navegador antes do envio (ADR 0058).
78
+ export { prepareImage } from "./lib/prepare-image.ts";
79
+ export type {
80
+ PrepareImageOptions,
81
+ PrepareImageRejection,
82
+ PrepareImageResult,
83
+ PreparedImage,
84
+ RejectedImage,
85
+ } from "./lib/prepare-image.ts";
69
86
  export { Copyable } from "./components/primitives/copyable.tsx";
70
87
  export type { CopyableProps } from "./components/primitives/copyable.tsx";
71
88
  export {
@@ -156,6 +173,7 @@ export {
156
173
  DialogBody,
157
174
  DialogFooter,
158
175
  DialogTitle,
176
+ DialogDescription,
159
177
  } from "./components/primitives/dialog.tsx";
160
178
  export type {
161
179
  DialogProps,
@@ -309,6 +327,7 @@ export {
309
327
  DrawerNavigation,
310
328
  DrawerBody,
311
329
  DrawerTitle,
330
+ DrawerDescription,
312
331
  DrawerTrigger,
313
332
  } from "./components/primitives/drawer.tsx";
314
333
  export {
@@ -492,6 +511,7 @@ export {
492
511
  PageNavigation,
493
512
  PageBack,
494
513
  PageTitle,
514
+ PageDescription,
495
515
  PageActions,
496
516
  PageActionsTarget,
497
517
  PageBody,