@adatechnology/conversations-ui 0.1.0-rc.8 → 0.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.
- package/dist/ConversationSimulatorPanel--5fIzXWY.d.ts +804 -0
- package/dist/chunk-BJNRLLDO.js +2708 -0
- package/dist/chunk-DKPXKQGC.js +110 -0
- package/dist/{chunk-OGRRHQQW.js → chunk-WCBDXZ3X.js} +68 -4
- package/dist/flows/index.d.ts +422 -5
- package/dist/flows/index.js +2502 -678
- package/dist/index.d.ts +921 -17
- package/dist/index.js +3677 -678
- package/dist/preview/index.d.ts +62 -105
- package/dist/preview/index.js +162 -284
- package/dist/styles.css +893 -0
- package/package.json +9 -8
- package/src/AudioPlayer.tsx +8 -0
- package/src/AudioRecorderButton.test.tsx +30 -0
- package/src/AudioRecorderButton.tsx +248 -0
- package/src/AudioTranscription.test.tsx +115 -0
- package/src/AudioTranscription.tsx +252 -0
- package/src/Avatar.tsx +1 -1
- package/src/ConversationContextPanel.tsx +218 -44
- package/src/ConversationDocumentsPanel.tsx +11 -6
- package/src/ConversationHeader.test.tsx +66 -0
- package/src/ConversationHeader.tsx +147 -47
- package/src/ConversationListItem.tsx +8 -6
- package/src/ConversationLocalesProvider.tsx +28 -0
- package/src/ConversationRow.tsx +53 -7
- package/src/DarkModeToggle.test.tsx +76 -0
- package/src/DarkModeToggle.tsx +92 -0
- package/src/DocumentsLibrary.tsx +67 -7
- package/src/EmojiPicker.tsx +2 -1
- package/src/InteractiveMessage.tsx +3 -0
- package/src/Lightbox.tsx +1 -1
- package/src/MediaRenderer.tsx +88 -15
- package/src/MessageBubble.test.tsx +41 -0
- package/src/MessageBubble.tsx +47 -5
- package/src/MessageComposer.test.tsx +35 -0
- package/src/MessageComposer.tsx +122 -17
- package/src/MessageText.tsx +2 -1
- package/src/MessageTimestamp.tsx +2 -1
- package/src/RichMessageComposer.test.tsx +113 -0
- package/src/RichMessageComposer.tsx +551 -0
- package/src/SimpleEmojiPicker.tsx +5 -3
- package/src/StatusTicks.tsx +1 -1
- package/src/Toast.tsx +4 -0
- package/src/Tooltip.test.ts +42 -0
- package/src/Tooltip.tsx +167 -0
- package/src/Wallpaper.test.tsx +21 -0
- package/src/Wallpaper.tsx +67 -7
- package/src/WhatsAppMessageEditor.tsx +10 -7
- package/src/WindowExpiredNotice.tsx +12 -4
- package/src/{preview/audioRecorderFormat.test.ts → audioRecorderFormat.test.ts} +1 -1
- package/src/buildOutput.test.ts +79 -0
- package/src/composer.constant.ts +33 -0
- package/src/conversationTranscript.test.ts +57 -0
- package/src/conversationTranscript.ts +29 -4
- package/src/conversationWindow.ts +7 -5
- package/src/documentTypeLabel.test.ts +57 -0
- package/src/documents/DocumentsWorkspace.tsx +550 -0
- package/src/documents/index.ts +8 -0
- package/src/documents/labels.ts +92 -0
- package/src/flows/FlowConnectionEdge.tsx +104 -0
- package/src/flows/FlowGroupHeader.tsx +12 -2
- package/src/flows/FlowLegend.tsx +125 -0
- package/src/flows/FlowMapCanvas.tsx +15 -12
- package/src/flows/FlowMapNode.tsx +4 -1
- package/src/flows/FlowNodeCard.tsx +219 -34
- package/src/flows/FlowNodePanel.tsx +153 -39
- package/src/flows/FlowPalette.tsx +156 -70
- package/src/flows/FlowPortalNode.tsx +1 -1
- package/src/flows/FlowWhatsAppPreview.tsx +14 -3
- package/src/flows/FlowsWorkspace.tsx +1255 -0
- package/src/flows/flowCanvasModel.test.ts +456 -0
- package/src/flows/flowCanvasModel.ts +378 -0
- package/src/flows/flowEditorOps.test.ts +276 -0
- package/src/flows/flowEditorOps.ts +202 -0
- package/src/flows/flowGraph.ts +78 -53
- package/src/flows/flowMenuPlacement.test.ts +130 -0
- package/src/flows/flowMenuPlacement.ts +86 -0
- package/src/flows/index.ts +51 -2
- package/src/flows/labels.ts +180 -0
- package/src/flows/workspaceContract.test.ts +126 -0
- package/src/hooks/useContainerWidth.ts +35 -0
- package/src/hooks/useConversationRealtime.ts +10 -8
- package/src/hooks/useScrollToLatestMessage.ts +127 -0
- package/src/hooks/useUrlFilterState.ts +107 -0
- package/src/icon.constant.ts +12 -0
- package/src/index.ts +100 -0
- package/src/lib/composer-formatting.test.ts +78 -0
- package/src/lib/composer-formatting.ts +145 -0
- package/src/lib/whatsapp-formatting.test.tsx +37 -0
- package/src/lib/whatsapp-formatting.tsx +28 -3
- package/src/listing/index.tsx +202 -0
- package/src/pagination.constant.ts +10 -0
- package/src/preview/ConversationPreview.tsx +84 -45
- package/src/preview/ConversationSimulatorClient.ts +143 -0
- package/src/preview/ConversationSimulatorPanel.test.tsx +55 -0
- package/src/preview/ConversationSimulatorPanel.tsx +131 -0
- package/src/preview/createPreviewBridgeClient.test.ts +92 -0
- package/src/preview/createPreviewBridgeClient.ts +124 -0
- package/src/preview/createPreviewMediaUploader.ts +82 -0
- package/src/preview/createPreviewWebhookClient.test.ts +96 -0
- package/src/preview/createPreviewWebhookClient.ts +99 -3
- package/src/preview/index.ts +36 -2
- package/src/preview/previewMediaUploader.test.ts +61 -0
- package/src/providers/ConversationsProvider.tsx +8 -6
- package/src/providers/types.ts +59 -2
- package/src/quickReply.test.ts +58 -0
- package/src/replyLatency.test.ts +71 -0
- package/src/replyLatency.ts +57 -0
- package/src/settings/MessagesWorkspace.tsx +571 -0
- package/src/settings/TopicsForm.tsx +2 -0
- package/src/settings/TranscriptionSettingsForm.test.tsx +81 -0
- package/src/settings/TranscriptionSettingsForm.tsx +190 -0
- package/src/settings/WelcomeFarewellForm.tsx +1 -0
- package/src/settings/WhatsAppCreateTemplateForm.tsx +1 -0
- package/src/settings/WhatsAppTemplateSettingsForm.tsx +5 -2
- package/src/settings/WhatsAppTemplatesSettings.test.tsx +61 -0
- package/src/settings/WhatsAppTemplatesSettings.tsx +22 -2
- package/src/styles.css +858 -0
- package/src/theme.ts +13 -0
- package/src/types.ts +26 -0
- package/src/workspace/BulkTemplateModal.tsx +132 -0
- package/src/workspace/ConversationPane.tsx +432 -0
- package/src/workspace/ConversationsInboxList.tsx +194 -0
- package/src/workspace/ConversationsWorkspace.tsx +423 -0
- package/src/workspace/index.ts +17 -0
- package/src/workspace/labels.test.ts +17 -0
- package/src/workspace/labels.ts +85 -0
- package/src/workspace/useConversationsInbox.ts +332 -0
- package/dist/chunk-73MW5HNT.js +0 -1717
- package/dist/chunk-NV2RZ5KT.js +0 -56
- package/dist/types-B5C1DLu1.d.ts +0 -365
- package/src/preview/AudioRecorderButton.tsx +0 -117
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vocabulário da biblioteca de arquivos. Sobrescrevível campo a campo: o produto troca "cliente"
|
|
3
|
+
* por "lead" ou o idioma inteiro sem manter uma cópia da tela para isso.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface DocumentsWorkspaceLabels {
|
|
7
|
+
readonly title: string
|
|
8
|
+
readonly subtitle: (total: number) => string
|
|
9
|
+
readonly searchPlaceholder: string
|
|
10
|
+
readonly empty: string
|
|
11
|
+
readonly noResults: string
|
|
12
|
+
readonly loading: string
|
|
13
|
+
readonly failure: string
|
|
14
|
+
readonly view: string
|
|
15
|
+
readonly download: string
|
|
16
|
+
readonly remove: string
|
|
17
|
+
readonly removeConfirm: (filename: string) => string
|
|
18
|
+
readonly openConversation: string
|
|
19
|
+
readonly sourceFilter: string
|
|
20
|
+
readonly categoryFilter: string
|
|
21
|
+
readonly startDate: string
|
|
22
|
+
readonly endDate: string
|
|
23
|
+
readonly clearFilters: string
|
|
24
|
+
readonly selectAllPage: string
|
|
25
|
+
readonly selectRow: string
|
|
26
|
+
readonly bulkSelected: (count: number) => string
|
|
27
|
+
readonly bulkClear: string
|
|
28
|
+
readonly bulkDownloadZip: string
|
|
29
|
+
readonly bulkRemove: (count: number) => string
|
|
30
|
+
readonly bulkRemoveConfirm: (count: number) => string
|
|
31
|
+
readonly upload: string
|
|
32
|
+
readonly uploadError: string
|
|
33
|
+
readonly columnFilename: string
|
|
34
|
+
readonly columnContact: string
|
|
35
|
+
readonly columnType: string
|
|
36
|
+
readonly columnSize: string
|
|
37
|
+
readonly columnSource: string
|
|
38
|
+
readonly columnDate: string
|
|
39
|
+
readonly columnActions: string
|
|
40
|
+
readonly sourceLabels: Readonly<Record<string, string>>
|
|
41
|
+
readonly categoryLabels: Readonly<Record<string, string>>
|
|
42
|
+
readonly show: string
|
|
43
|
+
readonly perPage: string
|
|
44
|
+
readonly total: (count: number) => string
|
|
45
|
+
readonly page: (current: number, last: number) => string
|
|
46
|
+
readonly previousPage: string
|
|
47
|
+
readonly nextPage: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const DEFAULT_DOCUMENTS_WORKSPACE_LABELS: DocumentsWorkspaceLabels = {
|
|
51
|
+
title: 'Documentos',
|
|
52
|
+
subtitle: (total) => `${total} arquivo${total === 1 ? '' : 's'} trocado${total === 1 ? '' : 's'}`,
|
|
53
|
+
searchPlaceholder: 'Buscar por nome do arquivo ou telefone',
|
|
54
|
+
empty: 'Nenhum arquivo trocado ainda.',
|
|
55
|
+
noResults: 'Nenhum arquivo encontrado para os filtros aplicados.',
|
|
56
|
+
loading: 'Carregando arquivos…',
|
|
57
|
+
failure: 'Não foi possível carregar os arquivos.',
|
|
58
|
+
view: 'Visualizar',
|
|
59
|
+
download: 'Baixar',
|
|
60
|
+
remove: 'Excluir',
|
|
61
|
+
removeConfirm: (filename) => `Excluir "${filename}"?`,
|
|
62
|
+
openConversation: 'Abrir conversa',
|
|
63
|
+
sourceFilter: 'Origem',
|
|
64
|
+
categoryFilter: 'Tipo',
|
|
65
|
+
startDate: 'De',
|
|
66
|
+
endDate: 'Até',
|
|
67
|
+
clearFilters: 'Limpar filtros',
|
|
68
|
+
selectAllPage: 'Selecionar todos desta página',
|
|
69
|
+
selectRow: 'Selecionar arquivo',
|
|
70
|
+
bulkSelected: (count) => `${count} selecionado${count === 1 ? '' : 's'}`,
|
|
71
|
+
bulkClear: 'Limpar seleção',
|
|
72
|
+
bulkDownloadZip: 'Baixar em zip',
|
|
73
|
+
bulkRemove: (count) => `Excluir ${count}`,
|
|
74
|
+
bulkRemoveConfirm: (count) => `Excluir ${count} arquivo${count === 1 ? '' : 's'}?`,
|
|
75
|
+
upload: 'Enviar documento',
|
|
76
|
+
uploadError: 'Não foi possível enviar o arquivo.',
|
|
77
|
+
columnFilename: 'Arquivo',
|
|
78
|
+
columnContact: 'Contato',
|
|
79
|
+
columnType: 'Tipo',
|
|
80
|
+
columnSize: 'Tamanho',
|
|
81
|
+
columnSource: 'Origem',
|
|
82
|
+
columnDate: 'Recebido em',
|
|
83
|
+
columnActions: 'Ações',
|
|
84
|
+
sourceLabels: { customer: 'Cliente', agent: 'Atendente', bot: 'Bot' },
|
|
85
|
+
categoryLabels: { document: 'Documento', image: 'Imagem', audio: 'Áudio', video: 'Vídeo' },
|
|
86
|
+
show: 'Mostrar',
|
|
87
|
+
perPage: 'por página',
|
|
88
|
+
total: (count) => `${count} no total`,
|
|
89
|
+
page: (current, last) => `${current} / ${last}`,
|
|
90
|
+
previousPage: 'Página anterior',
|
|
91
|
+
nextPage: 'Próxima página',
|
|
92
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 Ada Technology. MIT License.
|
|
3
|
+
*
|
|
4
|
+
* A aresta do editor, com o botão de desligar em cima dela.
|
|
5
|
+
*
|
|
6
|
+
* Existe porque desligar um fio não tinha caminho nenhum no canvas: as arestas são derivadas do
|
|
7
|
+
* grafo a cada render, então clicar e apertar Delete não tem onde guardar a seleção. O botão no
|
|
8
|
+
* hover resolve sem estado de seleção — e é o único lugar em que a pessoa já está olhando quando
|
|
9
|
+
* quer trocar o destino.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useEffect, useRef, useState } from 'react'
|
|
13
|
+
import { BaseEdge, EdgeLabelRenderer, getBezierPath, type EdgeProps } from '@xyflow/react'
|
|
14
|
+
import { X } from 'lucide-react'
|
|
15
|
+
|
|
16
|
+
/** Folga para o ponteiro sair do traço e alcançar o botão sem que ele desapareça no caminho. */
|
|
17
|
+
const HIDE_DELAY_MS = 320
|
|
18
|
+
|
|
19
|
+
export type FlowConnectionEdgeData = {
|
|
20
|
+
/** Ausente em aresta que não se desliga daqui (salto entre fluxos via portal). */
|
|
21
|
+
readonly onDisconnect?: (() => void) | undefined
|
|
22
|
+
readonly disconnectLabel: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function FlowConnectionEdge({
|
|
26
|
+
id,
|
|
27
|
+
sourceX,
|
|
28
|
+
sourceY,
|
|
29
|
+
targetX,
|
|
30
|
+
targetY,
|
|
31
|
+
sourcePosition,
|
|
32
|
+
targetPosition,
|
|
33
|
+
markerEnd,
|
|
34
|
+
style,
|
|
35
|
+
data,
|
|
36
|
+
interactionWidth,
|
|
37
|
+
}: EdgeProps) {
|
|
38
|
+
const [hovered, setHovered] = useState(false)
|
|
39
|
+
const hideTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
40
|
+
const { onDisconnect, disconnectLabel } = (data ?? {}) as FlowConnectionEdgeData
|
|
41
|
+
|
|
42
|
+
// Sair do traço não esconde o botão na hora: entre o fio e o botão há um vão de alguns pixels, e
|
|
43
|
+
// esconder no `mouseleave` seco fazia o alvo fugir do mouse a caminho dele.
|
|
44
|
+
function show() {
|
|
45
|
+
if (hideTimer.current) clearTimeout(hideTimer.current)
|
|
46
|
+
setHovered(true)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function scheduleHide() {
|
|
50
|
+
if (hideTimer.current) clearTimeout(hideTimer.current)
|
|
51
|
+
hideTimer.current = setTimeout(() => setHovered(false), HIDE_DELAY_MS)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
useEffect(() => () => clearTimeout(hideTimer.current), [])
|
|
55
|
+
|
|
56
|
+
const [path, labelX, labelY] = getBezierPath({
|
|
57
|
+
sourceX,
|
|
58
|
+
sourceY,
|
|
59
|
+
sourcePosition,
|
|
60
|
+
targetX,
|
|
61
|
+
targetY,
|
|
62
|
+
targetPosition,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<>
|
|
67
|
+
<BaseEdge id={id} path={path} markerEnd={markerEnd} style={style} interactionWidth={interactionWidth ?? 20} />
|
|
68
|
+
|
|
69
|
+
{/* Faixa invisível larga por cima do traço: o traço tem ~1,5px e mirar nele com o mouse é
|
|
70
|
+
tarefa de precisão que ninguém deveria ter. */}
|
|
71
|
+
<path
|
|
72
|
+
d={path}
|
|
73
|
+
fill="none"
|
|
74
|
+
strokeWidth={22}
|
|
75
|
+
stroke="transparent"
|
|
76
|
+
className="react-flow__edge-interaction"
|
|
77
|
+
onMouseEnter={show}
|
|
78
|
+
onMouseLeave={scheduleHide}
|
|
79
|
+
/>
|
|
80
|
+
|
|
81
|
+
{onDisconnect && hovered && (
|
|
82
|
+
<EdgeLabelRenderer>
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
data-cv-tooltip={disconnectLabel}
|
|
86
|
+
aria-label={disconnectLabel}
|
|
87
|
+
onMouseEnter={show}
|
|
88
|
+
onMouseLeave={scheduleHide}
|
|
89
|
+
onClick={(event) => {
|
|
90
|
+
event.stopPropagation()
|
|
91
|
+
onDisconnect()
|
|
92
|
+
}}
|
|
93
|
+
style={{ transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)` }}
|
|
94
|
+
className="nodrag nopan pointer-events-auto absolute flex h-5 w-5 items-center justify-center rounded-full border border-gray-300 bg-white text-gray-500 shadow-sm hover:border-red-400 hover:text-red-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400"
|
|
95
|
+
>
|
|
96
|
+
<X size={12} strokeWidth={2.5} />
|
|
97
|
+
</button>
|
|
98
|
+
</EdgeLabelRenderer>
|
|
99
|
+
)}
|
|
100
|
+
</>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const flowEdgeTypes = { flowConnection: FlowConnectionEdge }
|
|
@@ -18,10 +18,20 @@ export function FlowGroupHeader({ data }: NodeProps) {
|
|
|
18
18
|
return (
|
|
19
19
|
<div className="flex items-center gap-2 rounded-full border border-cyan-300 dark:border-cyan-700 bg-cyan-50 dark:bg-cyan-950/50 px-3 py-1 text-xs font-medium text-cyan-800 dark:text-cyan-200 shadow-sm whitespace-nowrap">
|
|
20
20
|
<span>{label}</span>
|
|
21
|
-
<button
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
onClick={onFocus}
|
|
24
|
+
data-cv-tooltip={labels.flowGroup.focus} aria-label={labels.flowGroup.focus}
|
|
25
|
+
className="hover:text-blue-600 dark:hover:text-blue-400"
|
|
26
|
+
>
|
|
22
27
|
<Maximize2 size={12} />
|
|
23
28
|
</button>
|
|
24
|
-
<button
|
|
29
|
+
<button
|
|
30
|
+
type="button"
|
|
31
|
+
onClick={onClose}
|
|
32
|
+
data-cv-tooltip={labels.flowGroup.close} aria-label={labels.flowGroup.close}
|
|
33
|
+
className="hover:text-red-600 dark:hover:text-red-400"
|
|
34
|
+
>
|
|
25
35
|
<X size={12} />
|
|
26
36
|
</button>
|
|
27
37
|
</div>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 Ada Technology. MIT License.
|
|
3
|
+
*
|
|
4
|
+
* Legenda do canvas.
|
|
5
|
+
*
|
|
6
|
+
* O editor distingue os tipos de ligação só por cor e traço — opção escolhida, fallback, salto entre
|
|
7
|
+
* fluxos, conversa viva — e nada na tela dizia o que era o quê. Quem não desenhou o fluxo lia um
|
|
8
|
+
* emaranhado colorido; a cor sem chave não informa, decora.
|
|
9
|
+
*
|
|
10
|
+
* Os contornos e marcas do card entram na mesma chave: card solto, início do fluxo e o ícone de
|
|
11
|
+
* repetição são vocabulário do editor tanto quanto as cores dos fios.
|
|
12
|
+
*
|
|
13
|
+
* Recolhida por padrão: em canvas cheio, uma caixa fixa cobre card. Quem já conhece o vocabulário
|
|
14
|
+
* não paga o espaço, e quem não conhece está a um clique.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useState } from 'react'
|
|
18
|
+
import { ChevronDown, ChevronUp, RotateCcw } from 'lucide-react'
|
|
19
|
+
|
|
20
|
+
import type { FlowEditorLabels } from './labels'
|
|
21
|
+
import type { FlowNodeType } from './flowGraph'
|
|
22
|
+
|
|
23
|
+
export type FlowLegendEdgeSample = {
|
|
24
|
+
readonly color: string
|
|
25
|
+
readonly dash?: string | undefined
|
|
26
|
+
readonly label: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type FlowLegendProps = {
|
|
30
|
+
readonly labels: FlowEditorLabels
|
|
31
|
+
readonly edgeSamples: readonly FlowLegendEdgeSample[]
|
|
32
|
+
/** Classe de cor por tipo de card — a mesma que o `FlowNodeCard` usa, para a chave bater. */
|
|
33
|
+
readonly nodeSwatches: readonly { readonly type: FlowNodeType; readonly className: string }[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function EdgeSample({ sample }: { sample: FlowLegendEdgeSample }) {
|
|
37
|
+
return (
|
|
38
|
+
<li className="flex items-center gap-2">
|
|
39
|
+
<svg width="28" height="12" viewBox="0 0 28 12" aria-hidden="true" className="shrink-0">
|
|
40
|
+
<line
|
|
41
|
+
x1="2"
|
|
42
|
+
y1="6"
|
|
43
|
+
x2="26"
|
|
44
|
+
y2="6"
|
|
45
|
+
stroke={sample.color}
|
|
46
|
+
strokeWidth="1.75"
|
|
47
|
+
{...(sample.dash ? { strokeDasharray: sample.dash } : {})}
|
|
48
|
+
/>
|
|
49
|
+
</svg>
|
|
50
|
+
<span className="text-[11px] leading-tight text-gray-600 dark:text-gray-300">{sample.label}</span>
|
|
51
|
+
</li>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function FlowLegend({ labels, edgeSamples, nodeSwatches }: FlowLegendProps) {
|
|
56
|
+
const [open, setOpen] = useState(false)
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="rounded-lg border border-gray-200 bg-white/95 shadow-sm backdrop-blur dark:border-gray-700 dark:bg-gray-800/95">
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
onClick={() => setOpen((current) => !current)}
|
|
63
|
+
data-cv-tooltip={labels.legendPanel.title}
|
|
64
|
+
aria-label={labels.legendPanel.title}
|
|
65
|
+
aria-expanded={open}
|
|
66
|
+
className="flex w-full items-center justify-between gap-2 px-2.5 py-1.5 text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"
|
|
67
|
+
>
|
|
68
|
+
{labels.legendPanel.title}
|
|
69
|
+
{open ? <ChevronDown size={13} /> : <ChevronUp size={13} />}
|
|
70
|
+
</button>
|
|
71
|
+
|
|
72
|
+
{open && (
|
|
73
|
+
<div className="max-h-72 w-64 overflow-y-auto border-t border-gray-100 px-2.5 py-2 dark:border-gray-700">
|
|
74
|
+
<p className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">
|
|
75
|
+
{labels.legendPanel.connections}
|
|
76
|
+
</p>
|
|
77
|
+
<ul className="space-y-1.5">
|
|
78
|
+
{edgeSamples.map((sample) => (
|
|
79
|
+
<EdgeSample key={sample.label} sample={sample} />
|
|
80
|
+
))}
|
|
81
|
+
</ul>
|
|
82
|
+
|
|
83
|
+
<p className="mb-1 mt-3 text-[10px] font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">
|
|
84
|
+
{labels.legendPanel.nodes}
|
|
85
|
+
</p>
|
|
86
|
+
<ul className="space-y-1.5">
|
|
87
|
+
{nodeSwatches.map((swatch) => (
|
|
88
|
+
<li key={swatch.type} className="flex items-center gap-2">
|
|
89
|
+
<span className={`h-3 w-5 shrink-0 rounded border-2 ${swatch.className}`} aria-hidden="true" />
|
|
90
|
+
<span className="text-[11px] leading-tight text-gray-600 dark:text-gray-300">
|
|
91
|
+
{labels.legend[swatch.type]}
|
|
92
|
+
</span>
|
|
93
|
+
</li>
|
|
94
|
+
))}
|
|
95
|
+
<li className="flex items-center gap-2">
|
|
96
|
+
<span
|
|
97
|
+
className="h-3 w-5 shrink-0 rounded border-2 border-dashed border-amber-400"
|
|
98
|
+
aria-hidden="true"
|
|
99
|
+
/>
|
|
100
|
+
<span className="text-[11px] leading-tight text-gray-600 dark:text-gray-300">
|
|
101
|
+
{labels.legendPanel.detached}
|
|
102
|
+
</span>
|
|
103
|
+
</li>
|
|
104
|
+
<li className="flex items-center gap-2">
|
|
105
|
+
<span className="flex h-3 w-5 shrink-0 items-center justify-center" aria-hidden="true">
|
|
106
|
+
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
|
|
107
|
+
</span>
|
|
108
|
+
<span className="text-[11px] leading-tight text-gray-600 dark:text-gray-300">
|
|
109
|
+
{labels.legendPanel.startNode}
|
|
110
|
+
</span>
|
|
111
|
+
</li>
|
|
112
|
+
<li className="flex items-center gap-2">
|
|
113
|
+
<span className="flex h-3 w-5 shrink-0 items-center justify-center text-gray-400" aria-hidden="true">
|
|
114
|
+
<RotateCcw size={12} strokeWidth={2.5} />
|
|
115
|
+
</span>
|
|
116
|
+
<span className="text-[11px] leading-tight text-gray-600 dark:text-gray-300">
|
|
117
|
+
{labels.legendPanel.selfLoop}
|
|
118
|
+
</span>
|
|
119
|
+
</li>
|
|
120
|
+
</ul>
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
@@ -27,18 +27,21 @@ export function FlowMapCanvas({ graphs, rootKey, onOpenFlow, labels: labelsOverr
|
|
|
27
27
|
const positions = useMemo(() => computeFlowMapLayout(graphs, rootKey), [graphs, rootKey])
|
|
28
28
|
|
|
29
29
|
const nodes = useMemo<Node[]>(
|
|
30
|
-
() =>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
() =>
|
|
31
|
+
Object.values(graphs).map(
|
|
32
|
+
(g): Node => ({
|
|
33
|
+
id: g.key,
|
|
34
|
+
type: 'flowMapNode',
|
|
35
|
+
position: positions[g.key] ?? { x: 0, y: 0 },
|
|
36
|
+
data: {
|
|
37
|
+
label: g.label,
|
|
38
|
+
nodeCount: Object.keys(g.nodes).length,
|
|
39
|
+
isRoot: g.key === rootKey,
|
|
40
|
+
labels,
|
|
41
|
+
onOpen: () => onOpenFlow(g.key),
|
|
42
|
+
} satisfies FlowMapNodeData,
|
|
43
|
+
}),
|
|
44
|
+
),
|
|
42
45
|
[graphs, positions, rootKey, onOpenFlow, labels],
|
|
43
46
|
)
|
|
44
47
|
|
|
@@ -21,10 +21,13 @@ export function FlowMapNode({ data }: NodeProps) {
|
|
|
21
21
|
<div className="flex items-center gap-1.5 text-emerald-700 dark:text-emerald-300">
|
|
22
22
|
<GitBranch size={14} />
|
|
23
23
|
<span className="text-sm font-semibold truncate">{label}</span>
|
|
24
|
-
{isRoot &&
|
|
24
|
+
{isRoot && (
|
|
25
|
+
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 shrink-0" data-cv-tooltip={labels.startNodeTooltip} />
|
|
26
|
+
)}
|
|
25
27
|
</div>
|
|
26
28
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">{labels.flowMap.nodeCount(nodeCount)}</p>
|
|
27
29
|
<button
|
|
30
|
+
data-cv-tooltip={labels.flowMap.openFlow} aria-label={labels.flowMap.openFlow}
|
|
28
31
|
type="button"
|
|
29
32
|
onClick={onOpen}
|
|
30
33
|
className="mt-2 inline-flex items-center gap-1 text-xs font-medium text-blue-600 dark:text-blue-400 hover:underline"
|