@snksergio/design-system 0.62.0 → 0.63.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 (135) hide show
  1. package/dist-lib/ai/blocos/chart/budget-breakdown.tsx +174 -0
  2. package/dist-lib/ai/blocos/indice.md +43 -0
  3. package/dist-lib/ai/blocos/paneldetail/detalhe-com-tabela.tsx +450 -0
  4. package/dist-lib/ai/blocos/paneldetail/detalhe-de-tarefa-com-abas.tsx +570 -0
  5. package/dist-lib/ai/blocos/paneldetail/detalhe-do-registro.tsx +555 -0
  6. package/dist-lib/ai/exemplos/app-shell/app-shell-example.tsx +140 -0
  7. package/dist-lib/ai/exemplos/app-shell/index.ts +3 -0
  8. package/dist-lib/ai/exemplos/app-shell/nav-data.ts +97 -0
  9. package/dist-lib/ai/exemplos/app-shell/routes.tsx +75 -0
  10. package/dist-lib/ai/exemplos/chat/chat-screen.tsx +152 -0
  11. package/dist-lib/ai/exemplos/chat/chat-v2-mocks.ts +171 -0
  12. package/dist-lib/ai/exemplos/chat/chat-v2.styles.ts +23 -0
  13. package/dist-lib/ai/exemplos/chat/chat-v2.types.ts +81 -0
  14. package/dist-lib/ai/exemplos/chat/components/ChannelDot/channel-dot.tsx +28 -0
  15. package/dist-lib/ai/exemplos/chat/components/ConversationActionsMenu/conversation-actions-menu.tsx +83 -0
  16. package/dist-lib/ai/exemplos/chat/components/ConversationActionsMenu/index.ts +4 -0
  17. package/dist-lib/ai/exemplos/chat/components/ConversationColumn/conversation-column.styles.ts +22 -0
  18. package/dist-lib/ai/exemplos/chat/components/ConversationColumn/conversation-column.tsx +203 -0
  19. package/dist-lib/ai/exemplos/chat/components/ConversationColumn/conversation-column.types.ts +10 -0
  20. package/dist-lib/ai/exemplos/chat/components/ConversationColumn/index.ts +2 -0
  21. package/dist-lib/ai/exemplos/chat/components/ConversationListItem/conversation-list-item.styles.ts +55 -0
  22. package/dist-lib/ai/exemplos/chat/components/ConversationListItem/conversation-list-item.tsx +52 -0
  23. package/dist-lib/ai/exemplos/chat/components/ConversationListItem/conversation-list-item.types.ts +7 -0
  24. package/dist-lib/ai/exemplos/chat/components/ConversationListItem/index.ts +2 -0
  25. package/dist-lib/ai/exemplos/chat/components/DateSeparator/date-separator.tsx +18 -0
  26. package/dist-lib/ai/exemplos/chat/components/DetailField/detail-field.tsx +23 -0
  27. package/dist-lib/ai/exemplos/chat/components/DetailSection/detail-section.tsx +41 -0
  28. package/dist-lib/ai/exemplos/chat/components/DetailSection/index.ts +1 -0
  29. package/dist-lib/ai/exemplos/chat/components/DetailsColumn/details-column.styles.ts +30 -0
  30. package/dist-lib/ai/exemplos/chat/components/DetailsColumn/details-column.tsx +142 -0
  31. package/dist-lib/ai/exemplos/chat/components/DetailsColumn/details-column.types.ts +16 -0
  32. package/dist-lib/ai/exemplos/chat/components/DetailsColumn/index.ts +5 -0
  33. package/dist-lib/ai/exemplos/chat/components/FilterRow/filter-row.styles.ts +37 -0
  34. package/dist-lib/ai/exemplos/chat/components/FilterRow/filter-row.tsx +45 -0
  35. package/dist-lib/ai/exemplos/chat/components/FilterRow/index.ts +1 -0
  36. package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-column.styles.ts +27 -0
  37. package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-column.tsx +66 -0
  38. package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-column.types.ts +13 -0
  39. package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-rail.tsx +38 -0
  40. package/dist-lib/ai/exemplos/chat/components/FiltersColumn/index.ts +6 -0
  41. package/dist-lib/ai/exemplos/chat/components/MessageBubble/index.ts +2 -0
  42. package/dist-lib/ai/exemplos/chat/components/MessageBubble/message-bubble.styles.ts +37 -0
  43. package/dist-lib/ai/exemplos/chat/components/MessageBubble/message-bubble.tsx +29 -0
  44. package/dist-lib/ai/exemplos/chat/components/MessageBubble/message-bubble.types.ts +5 -0
  45. package/dist-lib/ai/exemplos/chat/components/PersonAvatar/index.ts +5 -0
  46. package/dist-lib/ai/exemplos/chat/components/PersonAvatar/person-avatar.tsx +31 -0
  47. package/dist-lib/ai/exemplos/chat/components/QueueColumn/index.ts +2 -0
  48. package/dist-lib/ai/exemplos/chat/components/QueueColumn/queue-column.styles.ts +19 -0
  49. package/dist-lib/ai/exemplos/chat/components/QueueColumn/queue-column.tsx +140 -0
  50. package/dist-lib/ai/exemplos/chat/components/QueueColumn/queue-column.types.ts +13 -0
  51. package/dist-lib/ai/exemplos/chat/components/RailItem/index.ts +1 -0
  52. package/dist-lib/ai/exemplos/chat/components/RailItem/rail-item.styles.ts +30 -0
  53. package/dist-lib/ai/exemplos/chat/components/RailItem/rail-item.tsx +42 -0
  54. package/dist-lib/ai/exemplos/chat/hooks/use-resizable.ts +98 -0
  55. package/dist-lib/ai/exemplos/chat/index.ts +1 -0
  56. package/dist-lib/ai/exemplos/clientes/_table-data.ts +59 -0
  57. package/dist-lib/ai/exemplos/clientes/clientes-screen.tsx +505 -0
  58. package/dist-lib/ai/exemplos/clientes/clientes-showcase-mocks.ts +117 -0
  59. package/dist-lib/ai/exemplos/clientes/clientes-showcase.styles.ts +13 -0
  60. package/dist-lib/ai/exemplos/clientes/clientes-showcase.types.ts +16 -0
  61. package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/detail-drawer.styles.ts +28 -0
  62. package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/detail-drawer.tsx +303 -0
  63. package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/detail-drawer.types.ts +14 -0
  64. package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/index.ts +2 -0
  65. package/dist-lib/ai/exemplos/clientes/components/NovoClienteDrawer/index.ts +2 -0
  66. package/dist-lib/ai/exemplos/clientes/components/NovoClienteDrawer/novo-cliente-drawer.tsx +207 -0
  67. package/dist-lib/ai/exemplos/clientes/index.ts +1 -0
  68. package/dist-lib/ai/exemplos/edit-page/components/StepNav.tsx +81 -0
  69. package/dist-lib/ai/exemplos/edit-page/components/section-card.tsx +85 -0
  70. package/dist-lib/ai/exemplos/edit-page/edit-page-screen.tsx +234 -0
  71. package/dist-lib/ai/exemplos/edit-page/index.ts +1 -0
  72. package/dist-lib/ai/exemplos/finance/_table-data.ts +57 -0
  73. package/dist-lib/ai/exemplos/finance/clientes-financeiro-mocks.ts +227 -0
  74. package/dist-lib/ai/exemplos/finance/clientes-financeiro.types.ts +75 -0
  75. package/dist-lib/ai/exemplos/finance/clientes-showcase-mocks.ts +117 -0
  76. package/dist-lib/ai/exemplos/finance/clientes-showcase.styles.ts +13 -0
  77. package/dist-lib/ai/exemplos/finance/clientes-showcase.types.ts +16 -0
  78. package/dist-lib/ai/exemplos/finance/components/EditarFinanceDrawer/editar-finance-drawer.tsx +241 -0
  79. package/dist-lib/ai/exemplos/finance/components/EditarFinanceDrawer/index.ts +5 -0
  80. package/dist-lib/ai/exemplos/finance/components/ExtratoExpansion/extrato-expansion.tsx +172 -0
  81. package/dist-lib/ai/exemplos/finance/components/ExtratoExpansion/index.ts +1 -0
  82. package/dist-lib/ai/exemplos/finance/components/FinanceDetailPanel/finance-detail-panel.tsx +241 -0
  83. package/dist-lib/ai/exemplos/finance/components/FinanceDetailPanel/index.ts +2 -0
  84. package/dist-lib/ai/exemplos/finance/components/NovoClienteDrawer/index.ts +2 -0
  85. package/dist-lib/ai/exemplos/finance/components/NovoClienteDrawer/novo-cliente-drawer.tsx +207 -0
  86. package/dist-lib/ai/exemplos/finance/components/SacarDialog/index.ts +2 -0
  87. package/dist-lib/ai/exemplos/finance/components/SacarDialog/sacar-dialog.tsx +346 -0
  88. package/dist-lib/ai/exemplos/finance/finance-screen.tsx +821 -0
  89. package/dist-lib/ai/exemplos/finance/index.ts +1 -0
  90. package/dist-lib/ai/exemplos/gantt/_gantt-data.tsx +402 -0
  91. package/dist-lib/ai/exemplos/gantt/gantt-screen.tsx +512 -0
  92. package/dist-lib/ai/exemplos/gantt/index.ts +1 -0
  93. package/dist-lib/ai/exemplos/login/index.ts +1 -0
  94. package/dist-lib/ai/exemplos/login/login-screen.tsx +246 -0
  95. package/dist-lib/ai/exemplos/mapa-rede/components/ConsultorDetailPanel/consultor-detail-panel.tsx +158 -0
  96. package/dist-lib/ai/exemplos/mapa-rede/components/ConsultorDetailPanel/index.ts +2 -0
  97. package/dist-lib/ai/exemplos/mapa-rede/index.ts +1 -0
  98. package/dist-lib/ai/exemplos/mapa-rede/mapa-de-rede-mocks.ts +655 -0
  99. package/dist-lib/ai/exemplos/mapa-rede/mapa-de-rede.types.ts +53 -0
  100. package/dist-lib/ai/exemplos/mapa-rede/mapa-rede-screen.tsx +227 -0
  101. package/dist-lib/ai/exemplos/order-detail/components/ActivityTab.tsx +77 -0
  102. package/dist-lib/ai/exemplos/order-detail/components/AttachmentsTab.tsx +45 -0
  103. package/dist-lib/ai/exemplos/order-detail/components/CommentsTab.tsx +111 -0
  104. package/dist-lib/ai/exemplos/order-detail/components/DetailsTab.tsx +189 -0
  105. package/dist-lib/ai/exemplos/order-detail/components/OverviewTab.tsx +200 -0
  106. package/dist-lib/ai/exemplos/order-detail/components/section-card.tsx +85 -0
  107. package/dist-lib/ai/exemplos/order-detail/index.ts +1 -0
  108. package/dist-lib/ai/exemplos/order-detail/order-detail-screen.tsx +119 -0
  109. package/dist-lib/ai/exemplos/order-detail/order-mocks.ts +186 -0
  110. package/dist-lib/ai/exemplos/order-detail/order.types.ts +118 -0
  111. package/dist-lib/ai/indice.json +30 -15
  112. package/dist-lib/ai/manifest.json +31 -5
  113. package/dist-lib/ai/roteiros/app-builder/roteiro.md +155 -0
  114. package/dist-lib/ai/roteiros/auth-builder/roteiro.md +42 -0
  115. package/dist-lib/ai/roteiros/cards/roteiro.md +34 -0
  116. package/dist-lib/ai/roteiros/charts/roteiro.md +32 -0
  117. package/dist-lib/ai/roteiros/chat/roteiro.md +31 -0
  118. package/dist-lib/ai/roteiros/crud-builder/blueprint.md +63 -0
  119. package/dist-lib/ai/roteiros/crud-builder/entrevista.md +139 -0
  120. package/dist-lib/ai/roteiros/crud-builder/geracao.md +113 -0
  121. package/dist-lib/ai/roteiros/crud-builder/roteiro.md +89 -0
  122. package/dist-lib/ai/roteiros/drawers/roteiro.md +41 -0
  123. package/dist-lib/ai/roteiros/list-builder/blueprint.md +94 -0
  124. package/dist-lib/ai/roteiros/list-builder/entrevista.md +188 -0
  125. package/dist-lib/ai/roteiros/list-builder/geracao.md +63 -0
  126. package/dist-lib/ai/roteiros/list-builder/roteiro.md +91 -0
  127. package/dist-lib/ai/roteiros/module-replicator/roteiro.md +56 -0
  128. package/dist-lib/ai/roteiros/page-detail/roteiro.md +32 -0
  129. package/dist-lib/ai/roteiros/page-edit/roteiro.md +31 -0
  130. package/dist-lib/ai/roteiros/screen-composer/roteiro.md +95 -0
  131. package/package.json +1 -1
  132. /package/dist-lib/ai/roteiros/{dashboard → dashboard-builder}/blueprint.md +0 -0
  133. /package/dist-lib/ai/roteiros/{dashboard → dashboard-builder}/entrevista.md +0 -0
  134. /package/dist-lib/ai/roteiros/{dashboard → dashboard-builder}/geracao.md +0 -0
  135. /package/dist-lib/ai/roteiros/{dashboard → dashboard-builder}/roteiro.md +0 -0
@@ -0,0 +1,30 @@
1
+ import { tv } from "@/utils/tv";
2
+
3
+ export const detailsColumnStyles = tv({
4
+ slots: {
5
+ root: "relative flex flex-col shrink-0 bg-bg-surface border border-border-subtle rounded-radius-xl overflow-hidden",
6
+ /** Resize handle no left edge — 4px. */
7
+ handle:
8
+ "absolute left-0 top-0 bottom-0 w-[4px] z-[1] bg-transparent cursor-col-resize hover:bg-bg-brand active:bg-bg-brand transition-colors duration-150 focus-visible:outline-none focus-visible:bg-bg-brand",
9
+ header:
10
+ "flex items-center gap-gp-md px-pad-2xl py-pad-lg border-b border-border-subtle shrink-0",
11
+ title: "m-0 text-title-md text-fg-default flex-1",
12
+ body: "flex-1 min-h-0 overflow-y-auto px-pad-2xl py-pad-md flex flex-col gap-gp-2xl",
13
+ contactHead: "flex flex-col items-center gap-gp-md pb-pad-md",
14
+ contactName: "text-body-md font-semibold text-fg-default text-center",
15
+ tagsRow: "flex flex-col gap-gp-sm pt-pad-xs",
16
+ tagsLabel: "text-body-xs font-normal text-fg-muted",
17
+ tagsChips: "flex items-center gap-gp-xs flex-wrap",
18
+ tagAddBtn:
19
+ "inline-flex items-center gap-[3px] px-pad-md h-[22px] rounded-radius-full border border-dashed border-border-input text-caption-sm font-medium text-fg-muted hover:bg-bg-muted hover:text-fg-default transition-colors",
20
+ historyEmpty: "text-body-xs font-normal text-fg-subtle text-center py-pad-md",
21
+ historyList: "m-0 p-0 list-none flex flex-col gap-gp-sm",
22
+ historyItem:
23
+ "flex gap-gp-md p-pad-md rounded-radius-md bg-bg-muted border border-border-subtle",
24
+ historyId:
25
+ "text-caption-sm text-fg-muted shrink-0 [font-variant-numeric:tabular-nums] pt-[1px]",
26
+ historyBody: "flex-1 min-w-0 flex flex-col gap-[1px]",
27
+ historySubject: "text-body-xs text-fg-default",
28
+ historyMeta: "text-caption-sm text-fg-muted",
29
+ },
30
+ });
@@ -0,0 +1,142 @@
1
+ import { ArrowLeft, Plus, X } from "lucide-react";
2
+ import { cn } from "@/lib/utils";
3
+ import { Button } from "@/components/ui/Button/button";
4
+ import { Chip } from "@/components/ui/Chip";
5
+ import { CHANNEL_LABEL } from "../../chat-v2-mocks";
6
+ import { DetailField } from "../DetailField/detail-field";
7
+ import { DetailSection } from "../DetailSection";
8
+ import { PersonAvatar } from "../PersonAvatar";
9
+ import { detailsColumnStyles } from "./details-column.styles";
10
+ import type { DetailsColumnProps } from "./details-column.types";
11
+
12
+ /**
13
+ * Coluna direita: header com título + close, resize handle no left edge (4px),
14
+ * body scrollável com 3 accordions (Contato, Atendimento, Histórico).
15
+ * Width controlado pelo hook `useResizable` no parent.
16
+ */
17
+ export function DetailsColumn({
18
+ conversation,
19
+ detail,
20
+ openSections,
21
+ onToggleSection,
22
+ onClose,
23
+ width,
24
+ onResizeStart,
25
+ onBackToChat,
26
+ className,
27
+ }: DetailsColumnProps) {
28
+ const s = detailsColumnStyles();
29
+
30
+ return (
31
+ <aside
32
+ aria-label="Detalhes do contato"
33
+ style={{ width }}
34
+ className={cn(s.root(), className)}
35
+ >
36
+ <button
37
+ type="button"
38
+ onMouseDown={onResizeStart}
39
+ aria-label="Redimensionar painel de detalhes"
40
+ className={s.handle()}
41
+ />
42
+
43
+ <header className={s.header()}>
44
+ {onBackToChat && (
45
+ <Button
46
+ color="secondary"
47
+ variant="ghost"
48
+ size="icon-sm"
49
+ aria-label="Voltar pra conversa"
50
+ onClick={onBackToChat}
51
+ className="md:hidden"
52
+ >
53
+ <ArrowLeft />
54
+ </Button>
55
+ )}
56
+ <h3 className={s.title()}>Detalhes</h3>
57
+ <Button
58
+ color="secondary"
59
+ variant="ghost"
60
+ size="icon-sm"
61
+ aria-label="Fechar painel"
62
+ onClick={onClose}
63
+ >
64
+ <X />
65
+ </Button>
66
+ </header>
67
+
68
+ <div className={s.body()}>
69
+ <DetailSection
70
+ title="Contato"
71
+ open={openSections.contact}
72
+ onToggle={() => onToggleSection("contact")}
73
+ >
74
+ <div className={s.contactHead()}>
75
+ <PersonAvatar
76
+ initials={conversation.initials}
77
+ hex={conversation.avatarHex}
78
+ size="lg"
79
+ />
80
+ <div className={s.contactName()}>{conversation.name}</div>
81
+ </div>
82
+ <DetailField label="Telefone" value={detail.phone} />
83
+ <DetailField label="Email" value={detail.email} />
84
+ <DetailField label="Primeiro contato" value={detail.firstContact} />
85
+ </DetailSection>
86
+
87
+ <DetailSection
88
+ title="Atendimento"
89
+ open={openSections.ticket}
90
+ onToggle={() => onToggleSection("ticket")}
91
+ >
92
+ <DetailField label="ID" value={conversation.id} />
93
+ <DetailField label="Status" value={conversation.statusLabel} />
94
+ <DetailField label="Atribuído a" value={detail.assignedTo} />
95
+ <DetailField label="Canal" value={CHANNEL_LABEL[conversation.channel]} />
96
+ <DetailField label="Criado em" value={detail.createdAt} />
97
+
98
+ <div className={s.tagsRow()}>
99
+ <span className={s.tagsLabel()}>Tags</span>
100
+ <div className={s.tagsChips()}>
101
+ <Chip color={conversation.tagKind} variant="soft" size="sm" shape="pill">
102
+ {conversation.tag}
103
+ </Chip>
104
+ {detail.extraTags.map((t) => (
105
+ <Chip key={t} color="info" variant="soft" size="sm" shape="pill">
106
+ {t}
107
+ </Chip>
108
+ ))}
109
+ <button type="button" className={s.tagAddBtn()}>
110
+ <Plus size={11} strokeWidth={2.2} /> tag
111
+ </button>
112
+ </div>
113
+ </div>
114
+ </DetailSection>
115
+
116
+ <DetailSection
117
+ title="Histórico"
118
+ open={openSections.history}
119
+ onToggle={() => onToggleSection("history")}
120
+ >
121
+ {detail.history.length === 0 ? (
122
+ <div className={s.historyEmpty()}>Nenhum atendimento anterior.</div>
123
+ ) : (
124
+ <ul className={s.historyList()}>
125
+ {detail.history.map((h) => (
126
+ <li key={h.id} className={s.historyItem()}>
127
+ <span className={s.historyId()}>{h.id}</span>
128
+ <div className={s.historyBody()}>
129
+ <span className={s.historySubject()}>{h.subject}</span>
130
+ <span className={s.historyMeta()}>
131
+ {h.date} · {h.status}
132
+ </span>
133
+ </div>
134
+ </li>
135
+ ))}
136
+ </ul>
137
+ )}
138
+ </DetailSection>
139
+ </div>
140
+ </aside>
141
+ );
142
+ }
@@ -0,0 +1,16 @@
1
+ import type { Conversation, ConversationDetail } from "../../chat-v2.types";
2
+
3
+ export type SectionsState = Record<"contact" | "ticket" | "history", boolean>;
4
+
5
+ export type DetailsColumnProps = {
6
+ conversation: Conversation;
7
+ detail: ConversationDetail;
8
+ openSections: SectionsState;
9
+ onToggleSection: (id: keyof SectionsState) => void;
10
+ onClose: () => void;
11
+ width: number;
12
+ onResizeStart: (e: React.MouseEvent) => void;
13
+ /** Em mobile: botão de voltar pro chat. Desktop esconde. */
14
+ onBackToChat?: () => void;
15
+ className?: string;
16
+ };
@@ -0,0 +1,5 @@
1
+ export { DetailsColumn } from "./details-column";
2
+ export type {
3
+ DetailsColumnProps,
4
+ SectionsState,
5
+ } from "./details-column.types";
@@ -0,0 +1,37 @@
1
+ import { tv, type VariantProps } from "@/utils/tv";
2
+
3
+ export const filterRowStyles = tv({
4
+ slots: {
5
+ button: [
6
+ "flex items-center gap-gp-md w-full",
7
+ "px-pad-md py-pad-sm rounded-radius-md text-left",
8
+ "transition-colors duration-150 cursor-pointer",
9
+ ].join(" "),
10
+ /** Leading slot (icon ou dot fallback). */
11
+ leading: "shrink-0 inline-flex items-center justify-center size-[18px]",
12
+ /** Label do filtro (ellipsis). */
13
+ label:
14
+ "flex-1 min-w-0 text-body-xs whitespace-nowrap overflow-hidden text-ellipsis",
15
+ /** Dot brand 6px pra "tem novidades". */
16
+ unreadDot: "size-[6px] rounded-radius-full bg-bg-brand shrink-0",
17
+ /** Contador à direita. */
18
+ count: "text-caption-sm text-fg-muted shrink-0 [font-variant-numeric:tabular-nums]",
19
+ /** Dot fallback (quando item não tem icon). */
20
+ fallbackDot: "size-[8px] rounded-radius-full",
21
+ },
22
+ variants: {
23
+ isActive: {
24
+ true: {
25
+ button: "bg-bg-muted dark:bg-bg-emphasis text-fg-default font-semibold",
26
+ },
27
+ false: {
28
+ button: "text-fg-muted hover:bg-bg-muted hover:text-fg-default",
29
+ },
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ isActive: false,
34
+ },
35
+ });
36
+
37
+ export type FilterRowVariants = VariantProps<typeof filterRowStyles>;
@@ -0,0 +1,45 @@
1
+ import type { FilterItem } from "../../chat-v2.types";
2
+ import { filterRowStyles } from "./filter-row.styles";
3
+
4
+ export type FilterRowProps = {
5
+ item: FilterItem;
6
+ isActive: boolean;
7
+ onClick: () => void;
8
+ };
9
+
10
+ /**
11
+ * Row clicável da `FiltersColumn` expandida. Layout: leading (icon ou dot) +
12
+ * label (ellipsis) + dot unread opcional + count tabular.
13
+ */
14
+ export function FilterRow({ item, isActive, onClick }: FilterRowProps) {
15
+ const Icon = item.icon;
16
+ const s = filterRowStyles({ isActive });
17
+
18
+ return (
19
+ <button
20
+ type="button"
21
+ onClick={onClick}
22
+ aria-pressed={isActive}
23
+ className={s.button()}
24
+ >
25
+ <span className={s.leading()}>
26
+ {Icon ? (
27
+ <Icon
28
+ size={15}
29
+ strokeWidth={1.8}
30
+ style={{ color: item.color ?? "var(--color-fg-muted)" }}
31
+ />
32
+ ) : (
33
+ <span
34
+ className={s.fallbackDot()}
35
+ style={{ background: item.color ?? "var(--color-fg-muted)" }}
36
+ aria-hidden
37
+ />
38
+ )}
39
+ </span>
40
+ <span className={s.label()}>{item.name}</span>
41
+ {item.hasUnread && <span className={s.unreadDot()} aria-label="tem novidades" />}
42
+ <span className={s.count()}>{item.count}</span>
43
+ </button>
44
+ );
45
+ }
@@ -0,0 +1 @@
1
+ export { FilterRow, type FilterRowProps } from "./filter-row";
@@ -0,0 +1,27 @@
1
+ import { tv } from "@/utils/tv";
2
+
3
+ export const filtersColumnStyles = tv({
4
+ slots: {
5
+ /** Aside raiz da versão expandida (280px). */
6
+ root: "flex flex-col w-[280px] shrink-0 bg-bg-surface border border-border-subtle rounded-radius-xl overflow-hidden",
7
+ /** Aside raiz da versão rail (64px). */
8
+ rail: "flex flex-col w-[64px] shrink-0 bg-bg-surface border border-border-subtle rounded-radius-xl overflow-hidden",
9
+ /** Container scrollável das seções (expandida). */
10
+ scroll: "flex-1 min-h-0 overflow-y-auto px-pad-md py-pad-md flex flex-col gap-gp-lg",
11
+ /** Container scrollável (rail). */
12
+ railScroll:
13
+ "flex-1 min-h-0 overflow-y-auto py-pad-md flex flex-col items-center gap-gp-sm",
14
+ /** Section wrapper (expandida). */
15
+ section: "flex flex-col gap-gp-2xs",
16
+ /** Header de section (título all-caps). Combo: `caption-xs` (10/400)
17
+ + override `font-bold uppercase tracking-[0.06em]` pra pattern strong. */
18
+ sectionTitle:
19
+ "px-pad-md pt-pad-2xs pb-pad-2xs text-caption-xs font-bold tracking-[0.06em] text-fg-subtle uppercase",
20
+ /** Group wrapper (rail) — divider entre grupos. */
21
+ railGroup: "w-full flex flex-col items-center gap-gp-xs px-pad-md",
22
+ /** Modifier de divisor pros grupos não-primeiros (rail). */
23
+ railGroupDivider: "pt-pad-sm border-t border-border-subtle",
24
+ /** Modifier de divisor pras sections não-primeiras (expandida). */
25
+ sectionDivider: "pt-pad-lg border-t border-border-subtle",
26
+ },
27
+ });
@@ -0,0 +1,66 @@
1
+ import { cn } from "@/lib/utils";
2
+ import { FILTER_GROUPS } from "../../chat-v2-mocks";
3
+ import type { FilterGroupKey, FilterItem } from "../../chat-v2.types";
4
+ import { FilterRow } from "../FilterRow";
5
+ import { filtersColumnStyles } from "./filters-column.styles";
6
+ import type { FiltersColumnProps, FiltersState } from "./filters-column.types";
7
+
8
+ /**
9
+ * Coluna esquerda expandida (280px) com as 4 seções de filtros definidas
10
+ * em `FILTER_GROUPS` (mocks).
11
+ */
12
+ export function FiltersColumn({
13
+ filters,
14
+ onToggleFilter,
15
+ className,
16
+ }: FiltersColumnProps) {
17
+ const s = filtersColumnStyles();
18
+ return (
19
+ <aside aria-label="Filtros" className={cn(s.root(), className)}>
20
+ <div className={s.scroll()}>
21
+ {FILTER_GROUPS.map((sec, i) => (
22
+ <FilterSection
23
+ key={sec.group}
24
+ title={sec.title}
25
+ group={sec.group}
26
+ items={sec.items}
27
+ filters={filters}
28
+ onToggle={onToggleFilter}
29
+ isFirst={i === 0}
30
+ />
31
+ ))}
32
+ </div>
33
+ </aside>
34
+ );
35
+ }
36
+
37
+ function FilterSection({
38
+ title,
39
+ group,
40
+ items,
41
+ filters,
42
+ onToggle,
43
+ isFirst,
44
+ }: {
45
+ title: string;
46
+ group: FilterGroupKey;
47
+ items: FilterItem[];
48
+ filters: FiltersState;
49
+ onToggle: (group: FilterGroupKey, id: string) => void;
50
+ isFirst?: boolean;
51
+ }) {
52
+ const s = filtersColumnStyles();
53
+ return (
54
+ <section className={cn(s.section(), !isFirst && s.sectionDivider())}>
55
+ <div className={s.sectionTitle()}>{title}</div>
56
+ {items.map((item) => (
57
+ <FilterRow
58
+ key={item.id}
59
+ item={item}
60
+ isActive={filters[group].has(item.id)}
61
+ onClick={() => onToggle(group, item.id)}
62
+ />
63
+ ))}
64
+ </section>
65
+ );
66
+ }
@@ -0,0 +1,13 @@
1
+ import type { FilterGroupKey } from "../../chat-v2.types";
2
+
3
+ /**
4
+ * Estado dos filtros — um Set de IDs por grupo. Centralizado no parent
5
+ * (`ChatScreen`) e passado pra baixo.
6
+ */
7
+ export type FiltersState = Record<FilterGroupKey, Set<string>>;
8
+
9
+ export type FiltersColumnProps = {
10
+ filters: FiltersState;
11
+ onToggleFilter: (group: FilterGroupKey, id: string) => void;
12
+ className?: string;
13
+ };
@@ -0,0 +1,38 @@
1
+ import { cn } from "@/lib/utils";
2
+ import { FILTER_GROUPS } from "../../chat-v2-mocks";
3
+ import { RailItem } from "../RailItem";
4
+ import { filtersColumnStyles } from "./filters-column.styles";
5
+ import type { FiltersColumnProps } from "./filters-column.types";
6
+
7
+ /**
8
+ * Coluna esquerda compacta (64px) — só ícones, divididos por grupo
9
+ * com divider top entre grupos. Consome `FILTER_GROUPS` (mocks).
10
+ */
11
+ export function FiltersRail({
12
+ filters,
13
+ onToggleFilter,
14
+ className,
15
+ }: FiltersColumnProps) {
16
+ const s = filtersColumnStyles();
17
+ return (
18
+ <aside aria-label="Filtros (compactos)" className={cn(s.rail(), className)}>
19
+ <div className={s.railScroll()}>
20
+ {FILTER_GROUPS.map(({ group, items }, gi) => (
21
+ <div
22
+ key={group}
23
+ className={cn(s.railGroup(), gi > 0 && s.railGroupDivider())}
24
+ >
25
+ {items.map((item) => (
26
+ <RailItem
27
+ key={item.id}
28
+ item={item}
29
+ isActive={filters[group].has(item.id)}
30
+ onClick={() => onToggleFilter(group, item.id)}
31
+ />
32
+ ))}
33
+ </div>
34
+ ))}
35
+ </div>
36
+ </aside>
37
+ );
38
+ }
@@ -0,0 +1,6 @@
1
+ export { FiltersColumn } from "./filters-column";
2
+ export { FiltersRail } from "./filters-rail";
3
+ export type {
4
+ FiltersColumnProps,
5
+ FiltersState,
6
+ } from "./filters-column.types";
@@ -0,0 +1,2 @@
1
+ export { MessageBubble } from "./message-bubble";
2
+ export type { MessageBubbleProps } from "./message-bubble.types";
@@ -0,0 +1,37 @@
1
+ import { tv, type VariantProps } from "@/utils/tv";
2
+
3
+ export const messageBubbleStyles = tv({
4
+ slots: {
5
+ /** Wrapper que controla o alinhamento horizontal (esquerda/direita). */
6
+ row: "flex",
7
+ /** Balão em si — variants definem cor + corner pointer. */
8
+ bubble:
9
+ "max-w-[70%] px-pad-xl py-pad-md rounded-radius-xl shadow-sh-sm",
10
+ /** Texto da mensagem. */
11
+ text: "text-body-md leading-[1.45] whitespace-pre-wrap break-words",
12
+ /** Footer com horário + status icon. */
13
+ meta: "flex items-center justify-end gap-[4px] mt-[2px] text-caption-xs",
14
+ /** Span do horário (tabular-nums pra alinhar). */
15
+ time: "[font-variant-numeric:tabular-nums]",
16
+ },
17
+ variants: {
18
+ from: {
19
+ me: {
20
+ row: "justify-end",
21
+ bubble: "bg-bg-brand text-fg-on-brand rounded-br-[4px]",
22
+ meta: "text-fg-on-brand opacity-80",
23
+ },
24
+ them: {
25
+ row: "justify-start",
26
+ bubble:
27
+ "bg-bg-surface text-fg-default border border-border-subtle rounded-bl-[4px]",
28
+ meta: "text-fg-muted",
29
+ },
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ from: "them",
34
+ },
35
+ });
36
+
37
+ export type MessageBubbleVariants = VariantProps<typeof messageBubbleStyles>;
@@ -0,0 +1,29 @@
1
+ import { Check, CheckCheck } from "lucide-react";
2
+ import { messageBubbleStyles } from "./message-bubble.styles";
3
+ import type { MessageBubbleProps } from "./message-bubble.types";
4
+
5
+ /**
6
+ * Balão de mensagem. Variant `from` (me|them) controla cor + alinhamento.
7
+ * Status icons (Check / CheckCheck) renderizam apenas pra `from="me"`.
8
+ */
9
+ export function MessageBubble({ msg }: MessageBubbleProps) {
10
+ const from = msg.from === "me" ? "me" : "them";
11
+ const { row, bubble, text, meta, time } = messageBubbleStyles({ from });
12
+
13
+ return (
14
+ <div className={row()}>
15
+ <div className={bubble()}>
16
+ <div className={text()}>{msg.text}</div>
17
+ <div className={meta()}>
18
+ <span className={time()}>{msg.time}</span>
19
+ {from === "me" &&
20
+ (msg.status === "read" ? (
21
+ <CheckCheck size={12} strokeWidth={2} />
22
+ ) : (
23
+ <Check size={12} strokeWidth={2} />
24
+ ))}
25
+ </div>
26
+ </div>
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,5 @@
1
+ import type { Message } from "../../chat-v2.types";
2
+
3
+ export type MessageBubbleProps = {
4
+ msg: Message;
5
+ };
@@ -0,0 +1,5 @@
1
+ export {
2
+ PersonAvatar,
3
+ type PersonAvatarProps,
4
+ type PersonAvatarSize,
5
+ } from "./person-avatar";
@@ -0,0 +1,31 @@
1
+ import { Avatar, AvatarFallback } from "@/components/shadcn/avatar";
2
+
3
+ export type PersonAvatarSize = "sm" | "md" | "lg";
4
+
5
+ export type PersonAvatarProps = {
6
+ initials: string;
7
+ /** Cor de fundo do avatar — hex/rgb/CSS color (vem do mock do contato). */
8
+ hex: string;
9
+ /** sm=28px / md=36px / lg=40px. Default: md. */
10
+ size?: PersonAvatarSize;
11
+ };
12
+
13
+ const SIZE_CLASSES: Record<PersonAvatarSize, string> = {
14
+ sm: "size-[28px] text-caption-sm",
15
+ md: "size-[36px] text-body-sm font-normal",
16
+ lg: "size-[40px] text-body-md",
17
+ };
18
+
19
+ /**
20
+ * Avatar circular com `initials` em branco bold sobre `hex` como background.
21
+ * Usado em ConversationListItem, header de ConversationColumn e DetailsColumn.
22
+ */
23
+ export function PersonAvatar({ initials, hex, size = "md" }: PersonAvatarProps) {
24
+ return (
25
+ <Avatar className={SIZE_CLASSES[size]} style={{ background: hex }}>
26
+ <AvatarFallback className="bg-transparent text-white font-bold">
27
+ {initials}
28
+ </AvatarFallback>
29
+ </Avatar>
30
+ );
31
+ }
@@ -0,0 +1,2 @@
1
+ export { QueueColumn } from "./queue-column";
2
+ export type { QueueColumnProps } from "./queue-column.types";
@@ -0,0 +1,19 @@
1
+ import { tv } from "@/utils/tv";
2
+
3
+ export const queueColumnStyles = tv({
4
+ slots: {
5
+ root: "flex flex-col w-[340px] shrink-0 bg-bg-surface border border-border-subtle rounded-radius-xl overflow-hidden",
6
+ header: "flex items-center gap-gp-sm px-pad-md pt-pad-md pb-pad-md",
7
+ title: "m-0 text-title-md text-fg-default flex-1 min-w-0",
8
+ searchWrap: "px-pad-md pb-pad-lg",
9
+ searchInner: "relative",
10
+ searchIcon:
11
+ "absolute left-pad-lg top-1/2 -translate-y-1/2 text-fg-muted pointer-events-none",
12
+ searchInput: "pl-[34px] pr-[30px]",
13
+ searchClear:
14
+ "absolute right-pad-md top-1/2 -translate-y-1/2 grid place-items-center size-[20px] rounded-radius-md text-fg-muted hover:bg-bg-muted hover:text-fg-default transition-colors",
15
+ chipsWrap: "px-pad-md pb-pad-2xl",
16
+ list: "flex-1 min-h-0 overflow-y-auto px-pad-md pb-pad-md flex flex-col gap-gp-xs",
17
+ empty: "py-pad-4xl px-pad-xl text-center text-body-xs font-normal text-fg-subtle",
18
+ },
19
+ });