@adatechnology/conversations-ui 0.1.0-rc.30 → 0.1.0-rc.31
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/{chunk-GY472G6E.js → chunk-CUYYYZWD.js} +1 -0
- package/dist/flows/index.d.ts +37 -10
- package/dist/flows/index.js +11 -2
- package/dist/index.js +13 -12
- package/dist/preview/index.js +1 -1
- package/dist/styles.css +80 -0
- package/package.json +1 -1
- package/src/documents/DocumentsWorkspace.tsx +18 -11
- package/src/flows/FlowsWorkspace.tsx +32 -12
- package/src/flows/flowCanvasModel.test.ts +19 -0
- package/src/flows/flowCanvasModel.ts +32 -6
- package/src/flows/index.ts +10 -3
- package/src/styles.css +79 -0
package/dist/flows/index.d.ts
CHANGED
|
@@ -345,13 +345,26 @@ type FlowNodePosition = {
|
|
|
345
345
|
readonly x: number;
|
|
346
346
|
readonly y: number;
|
|
347
347
|
};
|
|
348
|
-
/** Onde cada conversa viva está parada agora. */
|
|
348
|
+
/** Onde cada conversa viva está parada agora — uma linha por sessão. */
|
|
349
349
|
type FlowLivePosition = {
|
|
350
350
|
readonly currentState: string;
|
|
351
351
|
readonly flow: string | null;
|
|
352
352
|
readonly nodeId: string | null;
|
|
353
353
|
readonly menuNodeId: string | null;
|
|
354
354
|
};
|
|
355
|
+
/**
|
|
356
|
+
* Quantas conversas estão paradas num nó — o formato que o servidor já agrega.
|
|
357
|
+
*
|
|
358
|
+
* `meta-whatsapp-module` responde `GROUP BY (flowKey, currentNodeId)`, e trazer uma linha por
|
|
359
|
+
* sessão só para o editor recontar em memória seria pior em toda base grande. Os dois formatos
|
|
360
|
+
* valem porque produto que já implementou o de sessão não deve quebrar numa release de correção.
|
|
361
|
+
*/
|
|
362
|
+
type FlowLiveNodeCount = {
|
|
363
|
+
readonly flowKey: string;
|
|
364
|
+
readonly nodeId: string;
|
|
365
|
+
readonly count: number;
|
|
366
|
+
};
|
|
367
|
+
type FlowLivePositionInput = FlowLivePosition | FlowLiveNodeCount;
|
|
355
368
|
/**
|
|
356
369
|
* Pseudo-nó que representa um fluxo alvo ainda não mesclado no canvas.
|
|
357
370
|
*
|
|
@@ -364,14 +377,15 @@ declare const GROUP_HEADER_NODE_ID = "__group_header__";
|
|
|
364
377
|
/**
|
|
365
378
|
* Quantas conversas estão paradas em cada nó de um fluxo.
|
|
366
379
|
*
|
|
367
|
-
* A raiz é caso à parte: o servidor guarda o passo do menu em `menuNodeId`,
|
|
368
|
-
* conversa no menu não carrega `flow`. Ler `nodeId` ali daria contagem
|
|
369
|
-
* editor.
|
|
380
|
+
* A raiz é caso à parte no formato por sessão: o servidor guarda o passo do menu em `menuNodeId`,
|
|
381
|
+
* num campo próprio, e uma conversa no menu não carrega `flow`. Ler `nodeId` ali daria contagem
|
|
382
|
+
* zero na tela mais visitada do editor. No formato agregado a chave do fluxo já vem na linha,
|
|
383
|
+
* então a raiz não tem exceção.
|
|
370
384
|
*/
|
|
371
385
|
declare function countLiveByNode(params: {
|
|
372
386
|
readonly flowKey: string;
|
|
373
387
|
readonly rootFlowKey: string;
|
|
374
|
-
readonly positions: readonly
|
|
388
|
+
readonly positions: readonly FlowLivePositionInput[] | undefined;
|
|
375
389
|
}): Record<string, number>;
|
|
376
390
|
/**
|
|
377
391
|
* Um layout só para TODOS os nós de TODOS os fluxos abertos juntos.
|
|
@@ -408,7 +422,7 @@ declare function buildFlowEdges(params: {
|
|
|
408
422
|
readonly openKeys: readonly string[];
|
|
409
423
|
readonly graphs: Readonly<Record<string, FlowGraphData>>;
|
|
410
424
|
readonly rootFlowKey: string;
|
|
411
|
-
readonly livePositions?: readonly
|
|
425
|
+
readonly livePositions?: readonly FlowLivePositionInput[] | undefined;
|
|
412
426
|
}): FlowEdgeSpec[];
|
|
413
427
|
/** Nós de um fluxo que ninguém aponta — o card ganha contorno tracejado para cobrar a ligação. */
|
|
414
428
|
declare function detachedNodeIds(graph: FlowGraphData): Set<string>;
|
|
@@ -453,8 +467,13 @@ interface FlowsWorkspaceApi {
|
|
|
453
467
|
*/
|
|
454
468
|
createFlow?(input: CreateFlowInput): Promise<void>;
|
|
455
469
|
deleteFlow?(key: string): Promise<void>;
|
|
456
|
-
/**
|
|
457
|
-
|
|
470
|
+
/**
|
|
471
|
+
* Onde estão as conversas vivas. Ausente, os cards não pulsam e nada é consultado.
|
|
472
|
+
*
|
|
473
|
+
* Aceita a linha por sessão e a linha já agregada por nó — `meta-whatsapp-module` responde a
|
|
474
|
+
* segunda, e exigir a primeira deixava os cards parados em todo produto que usa o módulo.
|
|
475
|
+
*/
|
|
476
|
+
getLivePositions?(): Promise<readonly FlowLivePositionInput[]>;
|
|
458
477
|
}
|
|
459
478
|
interface FlowsWorkspaceProps {
|
|
460
479
|
readonly api: FlowsWorkspaceApi;
|
|
@@ -467,6 +486,14 @@ interface FlowsWorkspaceProps {
|
|
|
467
486
|
readonly renderMediaPicker?: (node: FlowNodeData, graph: FlowGraphData) => ReactNode;
|
|
468
487
|
/** Intervalo do polling de posições vivas. Só tem efeito com `getLivePositions`. */
|
|
469
488
|
readonly livePollIntervalMs?: number;
|
|
489
|
+
/**
|
|
490
|
+
* Título e subtítulo do editor. `false` deixa só a barra de ações.
|
|
491
|
+
*
|
|
492
|
+
* Produto cuja navegação já nomeia a tela mostrava o nome duas vezes, em dois tamanhos, porque a
|
|
493
|
+
* tipografia daqui é do pacote e a de lá é do host. Esconder é a única saída que não força o
|
|
494
|
+
* pacote a adivinhar a escala tipográfica de cada produto.
|
|
495
|
+
*/
|
|
496
|
+
readonly showHeader?: boolean;
|
|
470
497
|
readonly className?: string;
|
|
471
498
|
}
|
|
472
499
|
/**
|
|
@@ -477,7 +504,7 @@ interface FlowsWorkspaceProps {
|
|
|
477
504
|
* versão diferente do mesmo editor. Customização entra por `labels`, `actionOptions` e
|
|
478
505
|
* `renderMediaPicker` — nunca por cópia do arquivo.
|
|
479
506
|
*/
|
|
480
|
-
declare function FlowsWorkspace({ api, rootFlowKey, labels: labelsOverride, actionOptions, renderMediaPicker, livePollIntervalMs, className, }: FlowsWorkspaceProps): react.JSX.Element;
|
|
507
|
+
declare function FlowsWorkspace({ api, rootFlowKey, labels: labelsOverride, actionOptions, renderMediaPicker, livePollIntervalMs, showHeader, className, }: FlowsWorkspaceProps): react.JSX.Element;
|
|
481
508
|
|
|
482
509
|
/**
|
|
483
510
|
* Copyright (c) 2026 Ada Technology. MIT License.
|
|
@@ -550,4 +577,4 @@ declare function applyConnection(node: FlowNodeData, resolved: ResolvedConnectio
|
|
|
550
577
|
*/
|
|
551
578
|
declare function isGraphDirty(working: FlowGraphData | undefined, published: FlowGraphData | undefined): boolean;
|
|
552
579
|
|
|
553
|
-
export { BUILT_IN_ACTION_KINDS, CONDITION_OPERATORS, CROSS_FLOW_PREFIX, type CollectionChain, type ConnectionRequest, type CreateFlowInput, DEFAULT_FLOW_EDITOR_LABELS, type FlowEdgeKind, type FlowEdgeSpec, type FlowEditorLabels, FlowGroupFrame, type FlowGroupFrameData, FlowGroupHeader, type FlowGroupHeaderData, type FlowLivePosition, FlowMapCanvas, type FlowMapCanvasProps, FlowMapNode, type FlowMapNodeData, FlowNodeCard, type FlowNodeCardData, FlowNodePanel, type FlowNodePanelProps, type FlowNodePosition, FlowPalette, type FlowPaletteActionOption, type FlowPaletteProps, FlowPortalNode, type FlowPortalNodeData, type FlowValidationLabels, FlowWhatsAppPreview, type FlowWhatsAppPreviewProps, FlowsWorkspace, type FlowsWorkspaceApi, type FlowsWorkspaceProps, GROUP_HEADER_NODE_ID, type GraphIssue, NODE_CARD_WIDTH, type NewNodeSpec, type ResolvedConnection, WHATSAPP_LIMITS, applyConnection, buildFlowEdges, chainFrameBounds, chainFrameNodeId, computeAutoLayout, computeFlowMapLayout, computeMergedLayout, countLiveByNode, crossFlowKey, crossFlowTargetsOf, detachedNodeIds, estimateNodeHeight, findCollectionChains, flowGroupFrameNodeTypes, flowGroupHeaderNodeTypes, flowMapNodeTypes, flowNodeTypes, flowPortalNodeTypes, isCrossFlowTarget, isGraphDirty, mergeFlowEditorLabels, mergedFlowKeysFrom, namespaceNodeId, newNodeFromSpec, nodeLabel, parseNamespacedId, portalNodeId, removeNodeAndCleanRefs, rendersAsButtons, resolveConnection, slugifyNodeId, targetsOf, validateGraph };
|
|
580
|
+
export { BUILT_IN_ACTION_KINDS, CONDITION_OPERATORS, CROSS_FLOW_PREFIX, type CollectionChain, type ConnectionRequest, type CreateFlowInput, DEFAULT_FLOW_EDITOR_LABELS, type FlowEdgeKind, type FlowEdgeSpec, type FlowEditorLabels, FlowGroupFrame, type FlowGroupFrameData, FlowGroupHeader, type FlowGroupHeaderData, type FlowLiveNodeCount, type FlowLivePosition, type FlowLivePositionInput, FlowMapCanvas, type FlowMapCanvasProps, FlowMapNode, type FlowMapNodeData, FlowNodeCard, type FlowNodeCardData, FlowNodePanel, type FlowNodePanelProps, type FlowNodePosition, FlowPalette, type FlowPaletteActionOption, type FlowPaletteProps, FlowPortalNode, type FlowPortalNodeData, type FlowValidationLabels, FlowWhatsAppPreview, type FlowWhatsAppPreviewProps, FlowsWorkspace, type FlowsWorkspaceApi, type FlowsWorkspaceProps, GROUP_HEADER_NODE_ID, type GraphIssue, NODE_CARD_WIDTH, type NewNodeSpec, type ResolvedConnection, WHATSAPP_LIMITS, applyConnection, buildFlowEdges, chainFrameBounds, chainFrameNodeId, computeAutoLayout, computeFlowMapLayout, computeMergedLayout, countLiveByNode, crossFlowKey, crossFlowTargetsOf, detachedNodeIds, estimateNodeHeight, findCollectionChains, flowGroupFrameNodeTypes, flowGroupHeaderNodeTypes, flowMapNodeTypes, flowNodeTypes, flowPortalNodeTypes, isCrossFlowTarget, isGraphDirty, mergeFlowEditorLabels, mergedFlowKeysFrom, namespaceNodeId, newNodeFromSpec, nodeLabel, parseNamespacedId, portalNodeId, removeNodeAndCleanRefs, rendersAsButtons, resolveConnection, slugifyNodeId, targetsOf, validateGraph };
|
package/dist/flows/index.js
CHANGED
|
@@ -1351,6 +1351,9 @@ function isGraphDirty(working, published) {
|
|
|
1351
1351
|
var COLUMN_GAP = 360;
|
|
1352
1352
|
var ROW_GAP = 40;
|
|
1353
1353
|
var CHAIN_LABEL_ROOM = 24;
|
|
1354
|
+
function isNodeCount(position) {
|
|
1355
|
+
return typeof position.count === "number";
|
|
1356
|
+
}
|
|
1354
1357
|
function portalNodeId(sourceNodeId, target) {
|
|
1355
1358
|
return `__portal__${sourceNodeId}__${target}`;
|
|
1356
1359
|
}
|
|
@@ -1363,6 +1366,11 @@ function countLiveByNode(params) {
|
|
|
1363
1366
|
const isRoot = flowKey === rootFlowKey;
|
|
1364
1367
|
const counts = {};
|
|
1365
1368
|
for (const position of positions ?? []) {
|
|
1369
|
+
if (isNodeCount(position)) {
|
|
1370
|
+
if (position.flowKey !== flowKey) continue;
|
|
1371
|
+
counts[position.nodeId] = (counts[position.nodeId] ?? 0) + position.count;
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1366
1374
|
if (position.flow !== flowKey && !isRoot) continue;
|
|
1367
1375
|
const nodeId = isRoot ? position.menuNodeId : position.nodeId;
|
|
1368
1376
|
if (!nodeId) continue;
|
|
@@ -1594,6 +1602,7 @@ function FlowsWorkspace({
|
|
|
1594
1602
|
actionOptions,
|
|
1595
1603
|
renderMediaPicker,
|
|
1596
1604
|
livePollIntervalMs = 5e3,
|
|
1605
|
+
showHeader = true,
|
|
1597
1606
|
className
|
|
1598
1607
|
}) {
|
|
1599
1608
|
const labels = useMemo2(() => mergeFlowEditorLabels(labelsOverride), [labelsOverride]);
|
|
@@ -2011,11 +2020,11 @@ function FlowsWorkspace({
|
|
|
2011
2020
|
return /* @__PURE__ */ jsxs9("div", { className: `space-y-4 h-full flex flex-col ${className ?? ""}`, children: [
|
|
2012
2021
|
/* @__PURE__ */ jsx10(TooltipLayer, {}),
|
|
2013
2022
|
/* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between flex-wrap gap-3", children: [
|
|
2014
|
-
/* @__PURE__ */ jsxs9("div", { children: [
|
|
2023
|
+
showHeader && /* @__PURE__ */ jsxs9("div", { className: "min-w-0", children: [
|
|
2015
2024
|
/* @__PURE__ */ jsx10("h2", { className: "text-2xl font-bold text-gray-900 dark:text-gray-100", children: labels.workspace.title }),
|
|
2016
2025
|
/* @__PURE__ */ jsx10("p", { className: "text-gray-500 dark:text-gray-400 text-sm mt-1", children: labels.workspace.subtitle })
|
|
2017
2026
|
] }),
|
|
2018
|
-
/* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-3", children: [
|
|
2027
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex flex-wrap items-center justify-end gap-3 ml-auto", children: [
|
|
2019
2028
|
saveState === "success" && /* @__PURE__ */ jsx10("span", { className: "text-sm text-green-600", children: labels.workspace.saveSuccess }),
|
|
2020
2029
|
saveState === "error" && /* @__PURE__ */ jsx10("span", { className: "text-sm text-red-600", children: saveErrorMessage ?? labels.workspace.saveError }),
|
|
2021
2030
|
/* @__PURE__ */ jsxs9(
|
package/dist/index.js
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
cn,
|
|
44
44
|
conversationsOf,
|
|
45
45
|
createMediaUrlResolver,
|
|
46
|
+
documentTypeLabel,
|
|
46
47
|
formatDateTime,
|
|
47
48
|
formatFileSize,
|
|
48
49
|
formatPhone,
|
|
@@ -57,7 +58,7 @@ import {
|
|
|
57
58
|
useConversationDocuments,
|
|
58
59
|
useConversationLocales,
|
|
59
60
|
useConversations
|
|
60
|
-
} from "./chunk-
|
|
61
|
+
} from "./chunk-CUYYYZWD.js";
|
|
61
62
|
import {
|
|
62
63
|
ZERO_WIDTH_SPACE,
|
|
63
64
|
htmlToWA,
|
|
@@ -2287,7 +2288,7 @@ function DocumentsWorkspace({
|
|
|
2287
2288
|
onChange: (event) => setSearch(event.target.value),
|
|
2288
2289
|
placeholder: labels.searchPlaceholder,
|
|
2289
2290
|
"aria-label": labels.searchPlaceholder,
|
|
2290
|
-
className: "w-full rounded-md border px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 sm:w-64"
|
|
2291
|
+
className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 sm:w-64"
|
|
2291
2292
|
}
|
|
2292
2293
|
),
|
|
2293
2294
|
/* @__PURE__ */ jsx16(MultiSelectFilter, { label: labels.sourceFilter, options: sourceOptions, selected: source, onChange: setSource }),
|
|
@@ -2308,7 +2309,7 @@ function DocumentsWorkspace({
|
|
|
2308
2309
|
value: startDate,
|
|
2309
2310
|
onChange: (event) => setStartDate(event.target.value),
|
|
2310
2311
|
"aria-label": labels.startDate,
|
|
2311
|
-
className: "rounded-md border px-2 py-2 text-xs dark:border-gray-700 dark:bg-gray-900"
|
|
2312
|
+
className: "rounded-md border border-gray-300 px-2 py-2 text-xs dark:border-gray-700 dark:bg-gray-900"
|
|
2312
2313
|
}
|
|
2313
2314
|
),
|
|
2314
2315
|
/* @__PURE__ */ jsx16(
|
|
@@ -2318,7 +2319,7 @@ function DocumentsWorkspace({
|
|
|
2318
2319
|
value: endDate,
|
|
2319
2320
|
onChange: (event) => setEndDate(event.target.value),
|
|
2320
2321
|
"aria-label": labels.endDate,
|
|
2321
|
-
className: "rounded-md border px-2 py-2 text-xs dark:border-gray-700 dark:bg-gray-900"
|
|
2322
|
+
className: "rounded-md border border-gray-300 px-2 py-2 text-xs dark:border-gray-700 dark:bg-gray-900"
|
|
2322
2323
|
}
|
|
2323
2324
|
)
|
|
2324
2325
|
] }) : null,
|
|
@@ -2404,9 +2405,9 @@ function DocumentsWorkspace({
|
|
|
2404
2405
|
) : null,
|
|
2405
2406
|
loading ? /* @__PURE__ */ jsx16("p", { className: cn("text-sm text-gray-500", classNames?.status), children: labels.loading }) : null,
|
|
2406
2407
|
failed ? /* @__PURE__ */ jsx16("p", { role: "alert", className: cn("text-sm text-red-600 dark:text-red-400", classNames?.status), children: labels.failure }) : null,
|
|
2407
|
-
/* @__PURE__ */ jsxs14("div", { className: cn("
|
|
2408
|
-
/* @__PURE__ */ jsxs14("table", { className: "
|
|
2409
|
-
/* @__PURE__ */ jsx16("thead", {
|
|
2408
|
+
/* @__PURE__ */ jsxs14("div", { className: cn("cv-table-card", classNames?.table), children: [
|
|
2409
|
+
/* @__PURE__ */ jsxs14("table", { className: "cv-table", children: [
|
|
2410
|
+
/* @__PURE__ */ jsx16("thead", { children: /* @__PURE__ */ jsxs14("tr", { children: [
|
|
2410
2411
|
canSelect ? /* @__PURE__ */ jsx16("th", { scope: "col", className: "w-10 px-3 py-2", children: /* @__PURE__ */ jsx16(
|
|
2411
2412
|
"input",
|
|
2412
2413
|
{
|
|
@@ -2425,7 +2426,7 @@ function DocumentsWorkspace({
|
|
|
2425
2426
|
/* @__PURE__ */ jsx16(SortableHead, { label: labels.columnDate, field: "linkedAt", activeField: sortField, direction, onSort: toggleSort, className: "hidden xl:table-cell" }),
|
|
2426
2427
|
/* @__PURE__ */ jsx16("th", { scope: "col", className: "px-3 py-2 text-left text-xs font-medium", children: labels.columnActions })
|
|
2427
2428
|
] }) }),
|
|
2428
|
-
/* @__PURE__ */ jsx16("tbody", { children: documents.map((document2) => /* @__PURE__ */ jsxs14("tr", { className:
|
|
2429
|
+
/* @__PURE__ */ jsx16("tbody", { children: documents.map((document2) => /* @__PURE__ */ jsxs14("tr", { className: classNames?.row, children: [
|
|
2429
2430
|
canSelect ? /* @__PURE__ */ jsx16("td", { className: "w-10 px-3 py-2", children: /* @__PURE__ */ jsx16(
|
|
2430
2431
|
"input",
|
|
2431
2432
|
{
|
|
@@ -2454,10 +2455,10 @@ function DocumentsWorkspace({
|
|
|
2454
2455
|
]
|
|
2455
2456
|
}
|
|
2456
2457
|
) : /* @__PURE__ */ jsx16("span", { className: "text-xs text-gray-500", children: document2.contactName ?? formatPhone(document2.conversationId) }) }),
|
|
2457
|
-
/* @__PURE__ */ jsx16("td", { className: "hidden px-3 py-2
|
|
2458
|
-
/* @__PURE__ */ jsx16("td", { className: "hidden px-3 py-2 text-xs lg:table-cell", children: formatFileSize(document2.sizeBytes) }),
|
|
2459
|
-
/* @__PURE__ */ jsx16("td", { className: "hidden px-3 py-2 text-xs lg:table-cell", children: labels.sourceLabels[document2.source] ?? document2.source }),
|
|
2460
|
-
/* @__PURE__ */ jsx16("td", { className: "hidden px-3 py-2 text-xs xl:table-cell", children: formatDateTime(document2.linkedAt) }),
|
|
2458
|
+
/* @__PURE__ */ jsx16("td", { className: "hidden px-3 py-2 md:table-cell", "data-cv-tooltip": document2.mimeType, children: /* @__PURE__ */ jsx16("span", { className: "cv-type-tag", children: documentTypeLabel(document2.filename, document2.mimeType) }) }),
|
|
2459
|
+
/* @__PURE__ */ jsx16("td", { className: "hidden whitespace-nowrap px-3 py-2 text-xs lg:table-cell", children: formatFileSize(document2.sizeBytes) }),
|
|
2460
|
+
/* @__PURE__ */ jsx16("td", { className: "hidden whitespace-nowrap px-3 py-2 text-xs lg:table-cell", children: labels.sourceLabels[document2.source] ?? document2.source }),
|
|
2461
|
+
/* @__PURE__ */ jsx16("td", { className: "hidden whitespace-nowrap px-3 py-2 text-xs xl:table-cell", children: formatDateTime(document2.linkedAt) }),
|
|
2461
2462
|
/* @__PURE__ */ jsx16("td", { className: "px-3 py-2", children: /* @__PURE__ */ jsxs14("div", { className: "flex gap-1", children: [
|
|
2462
2463
|
/* @__PURE__ */ jsx16(
|
|
2463
2464
|
"button",
|
package/dist/preview/index.js
CHANGED
package/dist/styles.css
CHANGED
|
@@ -981,6 +981,86 @@
|
|
|
981
981
|
.dark .cv-listing-perpage {
|
|
982
982
|
border-color: rgb(71 85 105);
|
|
983
983
|
}
|
|
984
|
+
:root {
|
|
985
|
+
--cv-border-color: rgb(229 231 235);
|
|
986
|
+
}
|
|
987
|
+
.dark {
|
|
988
|
+
--cv-border-color: rgb(51 65 85);
|
|
989
|
+
}
|
|
990
|
+
@layer base {
|
|
991
|
+
*,
|
|
992
|
+
::before,
|
|
993
|
+
::after,
|
|
994
|
+
::backdrop,
|
|
995
|
+
::file-selector-button {
|
|
996
|
+
border-color: var(--cv-border-color);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
.cv-table-card {
|
|
1000
|
+
overflow-x: auto;
|
|
1001
|
+
border: 1px solid rgb(229 231 235);
|
|
1002
|
+
border-radius: 0.75rem;
|
|
1003
|
+
background: #fff;
|
|
1004
|
+
}
|
|
1005
|
+
.dark .cv-table-card {
|
|
1006
|
+
border-color: rgb(51 65 85);
|
|
1007
|
+
background: rgb(17 24 39);
|
|
1008
|
+
}
|
|
1009
|
+
.cv-table {
|
|
1010
|
+
width: 100%;
|
|
1011
|
+
border-collapse: collapse;
|
|
1012
|
+
font-size: 0.875rem;
|
|
1013
|
+
}
|
|
1014
|
+
.cv-table thead {
|
|
1015
|
+
background: rgb(248 250 252);
|
|
1016
|
+
color: rgb(100 116 139);
|
|
1017
|
+
}
|
|
1018
|
+
.dark .cv-table thead {
|
|
1019
|
+
background: rgb(30 41 59 / 60%);
|
|
1020
|
+
color: rgb(148 163 184);
|
|
1021
|
+
}
|
|
1022
|
+
.cv-table thead th {
|
|
1023
|
+
border-bottom: 1px solid rgb(226 232 240);
|
|
1024
|
+
white-space: nowrap;
|
|
1025
|
+
}
|
|
1026
|
+
.dark .cv-table thead th {
|
|
1027
|
+
border-bottom-color: rgb(51 65 85);
|
|
1028
|
+
}
|
|
1029
|
+
.cv-table tbody tr {
|
|
1030
|
+
border-bottom: 1px solid rgb(241 245 249);
|
|
1031
|
+
}
|
|
1032
|
+
.dark .cv-table tbody tr {
|
|
1033
|
+
border-bottom-color: rgb(30 41 59);
|
|
1034
|
+
}
|
|
1035
|
+
.cv-table tbody tr:last-child {
|
|
1036
|
+
border-bottom: 0;
|
|
1037
|
+
}
|
|
1038
|
+
.cv-table tbody tr:nth-child(even) {
|
|
1039
|
+
background: rgb(248 250 252 / 70%);
|
|
1040
|
+
}
|
|
1041
|
+
.dark .cv-table tbody tr:nth-child(even) {
|
|
1042
|
+
background: rgb(30 41 59 / 35%);
|
|
1043
|
+
}
|
|
1044
|
+
.cv-table tbody tr:hover {
|
|
1045
|
+
background: rgb(239 246 255);
|
|
1046
|
+
}
|
|
1047
|
+
.dark .cv-table tbody tr:hover {
|
|
1048
|
+
background: rgb(30 58 138 / 25%);
|
|
1049
|
+
}
|
|
1050
|
+
.cv-type-tag {
|
|
1051
|
+
display: inline-block;
|
|
1052
|
+
border-radius: 0.25rem;
|
|
1053
|
+
background: rgb(241 245 249);
|
|
1054
|
+
padding: 0.125rem 0.375rem;
|
|
1055
|
+
font-size: 0.6875rem;
|
|
1056
|
+
font-weight: 600;
|
|
1057
|
+
letter-spacing: 0.02em;
|
|
1058
|
+
color: rgb(71 85 105);
|
|
1059
|
+
}
|
|
1060
|
+
.dark .cv-type-tag {
|
|
1061
|
+
background: rgb(51 65 85);
|
|
1062
|
+
color: rgb(203 213 225);
|
|
1063
|
+
}
|
|
984
1064
|
.cv-tooltip {
|
|
985
1065
|
position: fixed;
|
|
986
1066
|
z-index: 9999;
|
package/package.json
CHANGED
|
@@ -14,6 +14,7 @@ import { Download, Eye, MessageSquare, Trash2, Upload, X } from 'lucide-react'
|
|
|
14
14
|
|
|
15
15
|
import { useConversations } from '../providers/ConversationsProvider'
|
|
16
16
|
import { FileIcon } from '../FileIcon'
|
|
17
|
+
import { documentTypeLabel } from '../MediaRenderer'
|
|
17
18
|
import { cn } from '../lib/cn'
|
|
18
19
|
import { formatDateTime, formatFileSize } from '../lib/format'
|
|
19
20
|
import { formatPhone } from '../lib/phone'
|
|
@@ -281,7 +282,7 @@ export function DocumentsWorkspace({
|
|
|
281
282
|
onChange={(event) => setSearch(event.target.value)}
|
|
282
283
|
placeholder={labels.searchPlaceholder}
|
|
283
284
|
aria-label={labels.searchPlaceholder}
|
|
284
|
-
className="w-full rounded-md border px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 sm:w-64"
|
|
285
|
+
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 sm:w-64"
|
|
285
286
|
/>
|
|
286
287
|
|
|
287
288
|
<MultiSelectFilter label={labels.sourceFilter} options={sourceOptions} selected={source} onChange={setSource} />
|
|
@@ -302,14 +303,14 @@ export function DocumentsWorkspace({
|
|
|
302
303
|
value={startDate}
|
|
303
304
|
onChange={(event) => setStartDate(event.target.value)}
|
|
304
305
|
aria-label={labels.startDate}
|
|
305
|
-
className="rounded-md border px-2 py-2 text-xs dark:border-gray-700 dark:bg-gray-900"
|
|
306
|
+
className="rounded-md border border-gray-300 px-2 py-2 text-xs dark:border-gray-700 dark:bg-gray-900"
|
|
306
307
|
/>
|
|
307
308
|
<input
|
|
308
309
|
type="date"
|
|
309
310
|
value={endDate}
|
|
310
311
|
onChange={(event) => setEndDate(event.target.value)}
|
|
311
312
|
aria-label={labels.endDate}
|
|
312
|
-
className="rounded-md border px-2 py-2 text-xs dark:border-gray-700 dark:bg-gray-900"
|
|
313
|
+
className="rounded-md border border-gray-300 px-2 py-2 text-xs dark:border-gray-700 dark:bg-gray-900"
|
|
313
314
|
/>
|
|
314
315
|
</>
|
|
315
316
|
) : null}
|
|
@@ -399,9 +400,9 @@ export function DocumentsWorkspace({
|
|
|
399
400
|
</p>
|
|
400
401
|
) : null}
|
|
401
402
|
|
|
402
|
-
<div className={cn('
|
|
403
|
-
<table className="
|
|
404
|
-
<thead
|
|
403
|
+
<div className={cn('cv-table-card', classNames?.table)}>
|
|
404
|
+
<table className="cv-table">
|
|
405
|
+
<thead>
|
|
405
406
|
<tr>
|
|
406
407
|
{canSelect ? (
|
|
407
408
|
<th scope="col" className="w-10 px-3 py-2">
|
|
@@ -427,7 +428,7 @@ export function DocumentsWorkspace({
|
|
|
427
428
|
</thead>
|
|
428
429
|
<tbody>
|
|
429
430
|
{documents.map((document) => (
|
|
430
|
-
<tr key={`${document.conversationId}:${document.id}`} className={
|
|
431
|
+
<tr key={`${document.conversationId}:${document.id}`} className={classNames?.row}>
|
|
431
432
|
{canSelect ? (
|
|
432
433
|
<td className="w-10 px-3 py-2">
|
|
433
434
|
<input
|
|
@@ -467,12 +468,18 @@ export function DocumentsWorkspace({
|
|
|
467
468
|
)}
|
|
468
469
|
</td>
|
|
469
470
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
471
|
+
{/* O tipo curto, não o MIME cru: `.docx` responde
|
|
472
|
+
`application/vnd.openxmlformats-officedocument.wordprocessingml.document`, que numa
|
|
473
|
+
célula sem quebra esticava a coluna a 556px e espremia todas as outras. O MIME
|
|
474
|
+
inteiro continua na dica. */}
|
|
475
|
+
<td className="hidden px-3 py-2 md:table-cell" data-cv-tooltip={document.mimeType}>
|
|
476
|
+
<span className="cv-type-tag">{documentTypeLabel(document.filename, document.mimeType)}</span>
|
|
477
|
+
</td>
|
|
478
|
+
<td className="hidden whitespace-nowrap px-3 py-2 text-xs lg:table-cell">{formatFileSize(document.sizeBytes)}</td>
|
|
479
|
+
<td className="hidden whitespace-nowrap px-3 py-2 text-xs lg:table-cell">
|
|
473
480
|
{labels.sourceLabels[document.source] ?? document.source}
|
|
474
481
|
</td>
|
|
475
|
-
<td className="hidden px-3 py-2 text-xs xl:table-cell">{formatDateTime(document.linkedAt)}</td>
|
|
482
|
+
<td className="hidden whitespace-nowrap px-3 py-2 text-xs xl:table-cell">{formatDateTime(document.linkedAt)}</td>
|
|
476
483
|
|
|
477
484
|
<td className="px-3 py-2">
|
|
478
485
|
<div className="flex gap-1">
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
portalNodeId,
|
|
37
37
|
GROUP_HEADER_NODE_ID,
|
|
38
38
|
type FlowEdgeSpec,
|
|
39
|
-
type
|
|
39
|
+
type FlowLivePositionInput,
|
|
40
40
|
} from './flowCanvasModel'
|
|
41
41
|
import {
|
|
42
42
|
applyConnection,
|
|
@@ -79,9 +79,10 @@ const EDGE_COLOR_CROSS_FLOW = '#06b6d4'
|
|
|
79
79
|
const BACKGROUND_COLOR_LIGHT = '#cbd5e1'
|
|
80
80
|
const BACKGROUND_COLOR_DARK = '#334155'
|
|
81
81
|
|
|
82
|
-
//
|
|
83
|
-
// que o produto implementa. Declarar nos dois lugares faria as duas formas
|
|
84
|
-
|
|
82
|
+
// Os formatos de posição viva moram no modelo, que é quem conta — e são reexportados aqui porque
|
|
83
|
+
// fazem parte da api que o produto implementa. Declarar nos dois lugares faria as duas formas
|
|
84
|
+
// divergirem em silêncio.
|
|
85
|
+
export type { FlowLivePosition, FlowLiveNodeCount, FlowLivePositionInput } from './flowCanvasModel'
|
|
85
86
|
|
|
86
87
|
export interface CreateFlowInput {
|
|
87
88
|
key: string
|
|
@@ -105,8 +106,13 @@ export interface FlowsWorkspaceApi {
|
|
|
105
106
|
*/
|
|
106
107
|
createFlow?(input: CreateFlowInput): Promise<void>
|
|
107
108
|
deleteFlow?(key: string): Promise<void>
|
|
108
|
-
/**
|
|
109
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Onde estão as conversas vivas. Ausente, os cards não pulsam e nada é consultado.
|
|
111
|
+
*
|
|
112
|
+
* Aceita a linha por sessão e a linha já agregada por nó — `meta-whatsapp-module` responde a
|
|
113
|
+
* segunda, e exigir a primeira deixava os cards parados em todo produto que usa o módulo.
|
|
114
|
+
*/
|
|
115
|
+
getLivePositions?(): Promise<readonly FlowLivePositionInput[]>
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
export interface FlowsWorkspaceProps {
|
|
@@ -122,6 +128,14 @@ export interface FlowsWorkspaceProps {
|
|
|
122
128
|
readonly renderMediaPicker?: (node: FlowNodeData, graph: FlowGraphData) => ReactNode
|
|
123
129
|
/** Intervalo do polling de posições vivas. Só tem efeito com `getLivePositions`. */
|
|
124
130
|
readonly livePollIntervalMs?: number
|
|
131
|
+
/**
|
|
132
|
+
* Título e subtítulo do editor. `false` deixa só a barra de ações.
|
|
133
|
+
*
|
|
134
|
+
* Produto cuja navegação já nomeia a tela mostrava o nome duas vezes, em dois tamanhos, porque a
|
|
135
|
+
* tipografia daqui é do pacote e a de lá é do host. Esconder é a única saída que não força o
|
|
136
|
+
* pacote a adivinhar a escala tipográfica de cada produto.
|
|
137
|
+
*/
|
|
138
|
+
readonly showHeader?: boolean
|
|
125
139
|
readonly className?: string
|
|
126
140
|
}
|
|
127
141
|
|
|
@@ -190,6 +204,7 @@ export function FlowsWorkspace({
|
|
|
190
204
|
actionOptions,
|
|
191
205
|
renderMediaPicker,
|
|
192
206
|
livePollIntervalMs = 5000,
|
|
207
|
+
showHeader = true,
|
|
193
208
|
className,
|
|
194
209
|
}: FlowsWorkspaceProps) {
|
|
195
210
|
const labels = useMemo(() => mergeFlowEditorLabels(labelsOverride), [labelsOverride])
|
|
@@ -197,7 +212,7 @@ export function FlowsWorkspace({
|
|
|
197
212
|
|
|
198
213
|
const [graphs, setGraphs] = useState<Record<string, FlowGraphData> | undefined>(undefined)
|
|
199
214
|
const [loadState, setLoadState] = useState<'loading' | 'ready' | 'error'>('loading')
|
|
200
|
-
const [livePositions, setLivePositions] = useState<
|
|
215
|
+
const [livePositions, setLivePositions] = useState<readonly FlowLivePositionInput[] | undefined>(undefined)
|
|
201
216
|
const [viewMode, setViewMode] = useState<'detail' | 'map'>('detail')
|
|
202
217
|
const [openFlowKeys, setOpenFlowKeys] = useState<readonly string[]>([rootFlowKey])
|
|
203
218
|
const [hasAutoMerged, setHasAutoMerged] = useState(false)
|
|
@@ -709,11 +724,16 @@ export function FlowsWorkspace({
|
|
|
709
724
|
<div className={`space-y-4 h-full flex flex-col ${className ?? ''}`}>
|
|
710
725
|
<TooltipLayer />
|
|
711
726
|
<div className="flex items-center justify-between flex-wrap gap-3">
|
|
712
|
-
|
|
713
|
-
<
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
727
|
+
{showHeader && (
|
|
728
|
+
<div className="min-w-0">
|
|
729
|
+
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100">{labels.workspace.title}</h2>
|
|
730
|
+
<p className="text-gray-500 dark:text-gray-400 text-sm mt-1">{labels.workspace.subtitle}</p>
|
|
731
|
+
</div>
|
|
732
|
+
)}
|
|
733
|
+
{/* Quatro botões passam de 580px e a tela mais estreita é de 375px: sem `flex-wrap` a barra
|
|
734
|
+
empurrava a página inteira para o lado. `ml-auto` mantém tudo à direita mesmo sem o
|
|
735
|
+
título ao lado, que é o caso de `showHeader={false}`. */}
|
|
736
|
+
<div className="flex flex-wrap items-center justify-end gap-3 ml-auto">
|
|
717
737
|
{saveState === 'success' && <span className="text-sm text-green-600">{labels.workspace.saveSuccess}</span>}
|
|
718
738
|
{saveState === 'error' && (
|
|
719
739
|
<span className="text-sm text-red-600">{saveErrorMessage ?? labels.workspace.saveError}</span>
|
|
@@ -171,6 +171,25 @@ describe('contagem ao vivo', () => {
|
|
|
171
171
|
it('sem posições devolve vazio, em vez de estourar', () => {
|
|
172
172
|
expect(countLiveByNode({ flowKey: ROOT, rootFlowKey: ROOT, positions: undefined })).toEqual({})
|
|
173
173
|
})
|
|
174
|
+
|
|
175
|
+
it('linha já agregada pelo servidor soma pelo count, inclusive na raiz', () => {
|
|
176
|
+
// `meta-whatsapp-module` responde `GROUP BY (flowKey, currentNodeId)`. Contando como uma sessão
|
|
177
|
+
// por linha, dez conversas paradas no mesmo nó apareciam como uma — ou como nenhuma, porque a
|
|
178
|
+
// linha agregada não tem `menuNodeId` para a raiz ler.
|
|
179
|
+
const positions = [
|
|
180
|
+
{ flowKey: ROOT, nodeId: 'saudacao', count: 7 },
|
|
181
|
+
{ flowKey: 'sim', nodeId: 'renda', count: 3 },
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
expect(countLiveByNode({ flowKey: ROOT, rootFlowKey: ROOT, positions })).toEqual({ saudacao: 7 })
|
|
185
|
+
expect(countLiveByNode({ flowKey: 'sim', rootFlowKey: ROOT, positions })).toEqual({ renda: 3 })
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('os dois formatos convivem na mesma resposta', () => {
|
|
189
|
+
const positions = [live({ flow: 'sim', nodeId: 'renda' }), { flowKey: 'sim', nodeId: 'renda', count: 2 }]
|
|
190
|
+
|
|
191
|
+
expect(countLiveByNode({ flowKey: 'sim', rootFlowKey: ROOT, positions })).toEqual({ renda: 3 })
|
|
192
|
+
})
|
|
174
193
|
})
|
|
175
194
|
|
|
176
195
|
describe('layout mesclado', () => {
|
|
@@ -30,7 +30,7 @@ const CHAIN_LABEL_ROOM = 24
|
|
|
30
30
|
|
|
31
31
|
export type FlowNodePosition = { readonly x: number; readonly y: number }
|
|
32
32
|
|
|
33
|
-
/** Onde cada conversa viva está parada agora. */
|
|
33
|
+
/** Onde cada conversa viva está parada agora — uma linha por sessão. */
|
|
34
34
|
export type FlowLivePosition = {
|
|
35
35
|
readonly currentState: string
|
|
36
36
|
readonly flow: string | null
|
|
@@ -38,6 +38,25 @@ export type FlowLivePosition = {
|
|
|
38
38
|
readonly menuNodeId: string | null
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Quantas conversas estão paradas num nó — o formato que o servidor já agrega.
|
|
43
|
+
*
|
|
44
|
+
* `meta-whatsapp-module` responde `GROUP BY (flowKey, currentNodeId)`, e trazer uma linha por
|
|
45
|
+
* sessão só para o editor recontar em memória seria pior em toda base grande. Os dois formatos
|
|
46
|
+
* valem porque produto que já implementou o de sessão não deve quebrar numa release de correção.
|
|
47
|
+
*/
|
|
48
|
+
export type FlowLiveNodeCount = {
|
|
49
|
+
readonly flowKey: string
|
|
50
|
+
readonly nodeId: string
|
|
51
|
+
readonly count: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type FlowLivePositionInput = FlowLivePosition | FlowLiveNodeCount
|
|
55
|
+
|
|
56
|
+
function isNodeCount(position: FlowLivePositionInput): position is FlowLiveNodeCount {
|
|
57
|
+
return typeof (position as FlowLiveNodeCount).count === 'number'
|
|
58
|
+
}
|
|
59
|
+
|
|
41
60
|
/**
|
|
42
61
|
* Pseudo-nó que representa um fluxo alvo ainda não mesclado no canvas.
|
|
43
62
|
*
|
|
@@ -57,20 +76,27 @@ export const GROUP_HEADER_NODE_ID = '__group_header__'
|
|
|
57
76
|
/**
|
|
58
77
|
* Quantas conversas estão paradas em cada nó de um fluxo.
|
|
59
78
|
*
|
|
60
|
-
* A raiz é caso à parte: o servidor guarda o passo do menu em `menuNodeId`,
|
|
61
|
-
* conversa no menu não carrega `flow`. Ler `nodeId` ali daria contagem
|
|
62
|
-
* editor.
|
|
79
|
+
* A raiz é caso à parte no formato por sessão: o servidor guarda o passo do menu em `menuNodeId`,
|
|
80
|
+
* num campo próprio, e uma conversa no menu não carrega `flow`. Ler `nodeId` ali daria contagem
|
|
81
|
+
* zero na tela mais visitada do editor. No formato agregado a chave do fluxo já vem na linha,
|
|
82
|
+
* então a raiz não tem exceção.
|
|
63
83
|
*/
|
|
64
84
|
export function countLiveByNode(params: {
|
|
65
85
|
readonly flowKey: string
|
|
66
86
|
readonly rootFlowKey: string
|
|
67
|
-
readonly positions: readonly
|
|
87
|
+
readonly positions: readonly FlowLivePositionInput[] | undefined
|
|
68
88
|
}): Record<string, number> {
|
|
69
89
|
const { flowKey, rootFlowKey, positions } = params
|
|
70
90
|
const isRoot = flowKey === rootFlowKey
|
|
71
91
|
const counts: Record<string, number> = {}
|
|
72
92
|
|
|
73
93
|
for (const position of positions ?? []) {
|
|
94
|
+
if (isNodeCount(position)) {
|
|
95
|
+
if (position.flowKey !== flowKey) continue
|
|
96
|
+
counts[position.nodeId] = (counts[position.nodeId] ?? 0) + position.count
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
|
|
74
100
|
if (position.flow !== flowKey && !isRoot) continue
|
|
75
101
|
const nodeId = isRoot ? position.menuNodeId : position.nodeId
|
|
76
102
|
if (!nodeId) continue
|
|
@@ -191,7 +217,7 @@ export function buildFlowEdges(params: {
|
|
|
191
217
|
readonly openKeys: readonly string[]
|
|
192
218
|
readonly graphs: Readonly<Record<string, FlowGraphData>>
|
|
193
219
|
readonly rootFlowKey: string
|
|
194
|
-
readonly livePositions?: readonly
|
|
220
|
+
readonly livePositions?: readonly FlowLivePositionInput[] | undefined
|
|
195
221
|
}): FlowEdgeSpec[] {
|
|
196
222
|
const { openKeys, graphs, rootFlowKey, livePositions } = params
|
|
197
223
|
const open = new Set(openKeys)
|
package/src/flows/index.ts
CHANGED
|
@@ -45,7 +45,14 @@ export type {
|
|
|
45
45
|
} from './flowGraph'
|
|
46
46
|
|
|
47
47
|
export type { FlowEditorLabels, FlowValidationLabels } from './labels'
|
|
48
|
-
export type {
|
|
48
|
+
export type {
|
|
49
|
+
FlowsWorkspaceProps,
|
|
50
|
+
FlowsWorkspaceApi,
|
|
51
|
+
FlowLivePosition,
|
|
52
|
+
FlowLiveNodeCount,
|
|
53
|
+
FlowLivePositionInput,
|
|
54
|
+
CreateFlowInput,
|
|
55
|
+
} from './FlowsWorkspace'
|
|
49
56
|
export type { FlowNodeCardData } from './FlowNodeCard'
|
|
50
57
|
export type { FlowMapNodeData } from './FlowMapNode'
|
|
51
58
|
export type { FlowMapCanvasProps } from './FlowMapCanvas'
|
|
@@ -89,6 +96,6 @@ export {
|
|
|
89
96
|
portalNodeId,
|
|
90
97
|
GROUP_HEADER_NODE_ID,
|
|
91
98
|
} from './flowCanvasModel'
|
|
92
|
-
//
|
|
93
|
-
// api — exportar de novo aqui daria dois caminhos para o mesmo tipo.
|
|
99
|
+
// Os formatos de posição viva já saem pelo `FlowsWorkspace`, que é onde o produto os encontra ao
|
|
100
|
+
// implementar a api — exportar de novo aqui daria dois caminhos para o mesmo tipo.
|
|
94
101
|
export type { FlowEdgeKind, FlowEdgeSpec, FlowNodePosition } from './flowCanvasModel'
|
package/src/styles.css
CHANGED
|
@@ -888,6 +888,85 @@
|
|
|
888
888
|
}
|
|
889
889
|
.dark .cv-listing-perpage { border-color: rgb(71 85 105); }
|
|
890
890
|
|
|
891
|
+
/**
|
|
892
|
+
* Cor padrão de borda para as classes `border` sem cor ao lado.
|
|
893
|
+
*
|
|
894
|
+
* O Tailwind v4 trocou o padrão de `gray-200` por `currentColor`, e os componentes daqui foram
|
|
895
|
+
* escritos no v3. Em produto que traz a base do shadcn nada aparece — ela já redefine isto; em
|
|
896
|
+
* produto sem shadcn, cada `border` sai na cor do texto, e a tabela de documentos vinha com régua
|
|
897
|
+
* azul-quase-preta em vez de cinza. Fica na camada `base`, então qualquer `border-*` do host ou
|
|
898
|
+
* daqui continua ganhando, e a variável deixa o produto escolher outra cor sem editar o pacote.
|
|
899
|
+
*/
|
|
900
|
+
:root { --cv-border-color: rgb(229 231 235); }
|
|
901
|
+
.dark { --cv-border-color: rgb(51 65 85); }
|
|
902
|
+
|
|
903
|
+
@layer base {
|
|
904
|
+
*,
|
|
905
|
+
::before,
|
|
906
|
+
::after,
|
|
907
|
+
::backdrop,
|
|
908
|
+
::file-selector-button {
|
|
909
|
+
border-color: var(--cv-border-color);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/* Tabela de listagem: a moldura, o cabeçalho e a zebra que `web.md` §7 exige, no stylesheet pelo
|
|
914
|
+
mesmo motivo do resto — host que não varre `node_modules` com `@source` não compila utilitário
|
|
915
|
+
nenhum do pacote, e a tabela sairia crua. */
|
|
916
|
+
.cv-table-card {
|
|
917
|
+
overflow-x: auto;
|
|
918
|
+
border: 1px solid rgb(229 231 235);
|
|
919
|
+
border-radius: 0.75rem;
|
|
920
|
+
background: #fff;
|
|
921
|
+
}
|
|
922
|
+
.dark .cv-table-card { border-color: rgb(51 65 85); background: rgb(17 24 39); }
|
|
923
|
+
|
|
924
|
+
/* `collapse` porque a régua de baixo mora no `tr`: com o `separate` padrão o navegador ignora borda
|
|
925
|
+
de linha e a tabela ficava sem separação nenhuma entre os registros. */
|
|
926
|
+
.cv-table {
|
|
927
|
+
width: 100%;
|
|
928
|
+
border-collapse: collapse;
|
|
929
|
+
font-size: 0.875rem;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
.cv-table thead {
|
|
933
|
+
background: rgb(248 250 252);
|
|
934
|
+
color: rgb(100 116 139);
|
|
935
|
+
}
|
|
936
|
+
.dark .cv-table thead { background: rgb(30 41 59 / 60%); color: rgb(148 163 184); }
|
|
937
|
+
|
|
938
|
+
.cv-table thead th {
|
|
939
|
+
border-bottom: 1px solid rgb(226 232 240);
|
|
940
|
+
white-space: nowrap;
|
|
941
|
+
}
|
|
942
|
+
.dark .cv-table thead th { border-bottom-color: rgb(51 65 85); }
|
|
943
|
+
|
|
944
|
+
.cv-table tbody tr { border-bottom: 1px solid rgb(241 245 249); }
|
|
945
|
+
.dark .cv-table tbody tr { border-bottom-color: rgb(30 41 59); }
|
|
946
|
+
.cv-table tbody tr:last-child { border-bottom: 0; }
|
|
947
|
+
|
|
948
|
+
/* A alternada é o que leva o olho do nome do arquivo até a coluna de ações; em tela larga a régua
|
|
949
|
+
sozinha não segura a linha. */
|
|
950
|
+
.cv-table tbody tr:nth-child(even) { background: rgb(248 250 252 / 70%); }
|
|
951
|
+
.dark .cv-table tbody tr:nth-child(even) { background: rgb(30 41 59 / 35%); }
|
|
952
|
+
|
|
953
|
+
.cv-table tbody tr:hover { background: rgb(239 246 255); }
|
|
954
|
+
.dark .cv-table tbody tr:hover { background: rgb(30 58 138 / 25%); }
|
|
955
|
+
|
|
956
|
+
/* Extensão do arquivo como etiqueta: `PDF` e `DOCX` têm larguras parecidas e a coluna para de dançar
|
|
957
|
+
a cada linha, o que o MIME cru não permitia. */
|
|
958
|
+
.cv-type-tag {
|
|
959
|
+
display: inline-block;
|
|
960
|
+
border-radius: 0.25rem;
|
|
961
|
+
background: rgb(241 245 249);
|
|
962
|
+
padding: 0.125rem 0.375rem;
|
|
963
|
+
font-size: 0.6875rem;
|
|
964
|
+
font-weight: 600;
|
|
965
|
+
letter-spacing: 0.02em;
|
|
966
|
+
color: rgb(71 85 105);
|
|
967
|
+
}
|
|
968
|
+
.dark .cv-type-tag { background: rgb(51 65 85); color: rgb(203 213 225); }
|
|
969
|
+
|
|
891
970
|
/* Dica de ação (ver `Tooltip.tsx`). Impressa em `document.body`, então a posição é da janela e o
|
|
892
971
|
`z-index` fica acima de qualquer painel do produto. */
|
|
893
972
|
.cv-tooltip {
|