@fayz-ai/ui 0.9.0 → 0.10.1

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 (156) hide show
  1. package/README.md +2 -2
  2. package/dist/analytics/AnalyticsCard.d.ts +10 -0
  3. package/dist/analytics/AnalyticsCard.d.ts.map +1 -0
  4. package/dist/analytics/AnalyticsControls.d.ts +18 -0
  5. package/dist/analytics/AnalyticsControls.d.ts.map +1 -0
  6. package/dist/analytics/AnalyticsGrid.d.ts +16 -0
  7. package/dist/analytics/AnalyticsGrid.d.ts.map +1 -0
  8. package/dist/analytics/context.d.ts +58 -0
  9. package/dist/analytics/context.d.ts.map +1 -0
  10. package/dist/analytics/derive.d.ts +81 -0
  11. package/dist/analytics/derive.d.ts.map +1 -0
  12. package/dist/analytics/index.d.ts +13 -0
  13. package/dist/analytics/index.d.ts.map +1 -0
  14. package/dist/analytics/range.d.ts +22 -0
  15. package/dist/analytics/range.d.ts.map +1 -0
  16. package/dist/{chunk-PVIB724L.js → chunk-22EVFMS3.js} +5 -3
  17. package/dist/chunk-22EVFMS3.js.map +1 -0
  18. package/dist/{chunk-G6OKZ7WL.js → chunk-6V5MGYXH.js} +13 -9
  19. package/dist/chunk-6V5MGYXH.js.map +1 -0
  20. package/dist/{chunk-KV3MHB6N.js → chunk-H63TOSQ7.js} +196 -53
  21. package/dist/chunk-H63TOSQ7.js.map +1 -0
  22. package/dist/{chunk-OUPJDGKE.js → chunk-I6CPKAOD.js} +134 -37
  23. package/dist/chunk-I6CPKAOD.js.map +1 -0
  24. package/dist/{chunk-YRVE4FOD.js → chunk-OKZ6WQB3.js} +3 -3
  25. package/dist/chunk-OKZ6WQB3.js.map +1 -0
  26. package/dist/{chunk-S5KNM4D2.js → chunk-SEIY2F5F.js} +235 -107
  27. package/dist/chunk-SEIY2F5F.js.map +1 -0
  28. package/dist/{chunk-75EFDLST.js → chunk-VAKWUUVC.js} +157 -6
  29. package/dist/chunk-VAKWUUVC.js.map +1 -0
  30. package/dist/dashboard/OnboardingWidget.d.ts +2 -14
  31. package/dist/dashboard/OnboardingWidget.d.ts.map +1 -1
  32. package/dist/dashboard/SidebarOnboarding.d.ts +28 -0
  33. package/dist/dashboard/SidebarOnboarding.d.ts.map +1 -0
  34. package/dist/dashboard/index.d.ts +2 -0
  35. package/dist/dashboard/index.d.ts.map +1 -1
  36. package/dist/dashboard/index.js +4 -4
  37. package/dist/dashboard/types.d.ts +18 -0
  38. package/dist/dashboard/types.d.ts.map +1 -1
  39. package/dist/dashboard/useOnboardingProgress.d.ts +28 -0
  40. package/dist/dashboard/useOnboardingProgress.d.ts.map +1 -0
  41. package/dist/icons.js +1 -1
  42. package/dist/index.d.ts +5 -4
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +960 -14
  45. package/dist/index.js.map +1 -1
  46. package/dist/layout/AppShell.d.ts +14 -1
  47. package/dist/layout/AppShell.d.ts.map +1 -1
  48. package/dist/layout/ModulePage.d.ts +11 -2
  49. package/dist/layout/ModulePage.d.ts.map +1 -1
  50. package/dist/layout/Sidebar.d.ts +8 -1
  51. package/dist/layout/Sidebar.d.ts.map +1 -1
  52. package/dist/layout/index.js +4 -4
  53. package/dist/lib/cn.d.ts.map +1 -0
  54. package/dist/preset.d.ts +3 -0
  55. package/dist/preset.d.ts.map +1 -0
  56. package/dist/primitives/data-table.d.ts +19 -4
  57. package/dist/primitives/data-table.d.ts.map +1 -1
  58. package/dist/primitives/date-picker.d.ts +4 -2
  59. package/dist/primitives/date-picker.d.ts.map +1 -1
  60. package/dist/primitives/filter-chip.d.ts +26 -0
  61. package/dist/primitives/filter-chip.d.ts.map +1 -0
  62. package/dist/primitives/header-tabs.d.ts +32 -0
  63. package/dist/primitives/header-tabs.d.ts.map +1 -0
  64. package/dist/primitives/hint.d.ts +25 -0
  65. package/dist/primitives/hint.d.ts.map +1 -0
  66. package/dist/primitives/index.d.ts +7 -0
  67. package/dist/primitives/index.d.ts.map +1 -1
  68. package/dist/primitives/index.js +4 -4
  69. package/dist/primitives/list-view.d.ts +14 -1
  70. package/dist/primitives/list-view.d.ts.map +1 -1
  71. package/dist/primitives/page-header.d.ts +23 -0
  72. package/dist/primitives/page-header.d.ts.map +1 -0
  73. package/dist/primitives/stat-tiles.d.ts +30 -0
  74. package/dist/primitives/stat-tiles.d.ts.map +1 -0
  75. package/dist/primitives/switch.d.ts +11 -0
  76. package/dist/primitives/switch.d.ts.map +1 -0
  77. package/dist/stores/index.d.ts +1 -0
  78. package/dist/stores/index.d.ts.map +1 -1
  79. package/dist/stores/page-header.store.d.ts +69 -0
  80. package/dist/stores/page-header.store.d.ts.map +1 -0
  81. package/package.json +6 -15
  82. package/dist/chunk-75EFDLST.js.map +0 -1
  83. package/dist/chunk-G6OKZ7WL.js.map +0 -1
  84. package/dist/chunk-KV3MHB6N.js.map +0 -1
  85. package/dist/chunk-OUPJDGKE.js.map +0 -1
  86. package/dist/chunk-PVIB724L.js.map +0 -1
  87. package/dist/chunk-S5KNM4D2.js.map +0 -1
  88. package/dist/chunk-YRVE4FOD.js.map +0 -1
  89. package/dist/utils/cn.d.ts.map +0 -1
  90. package/src/dashboard/ChartWidget.tsx +0 -50
  91. package/src/dashboard/ChartWidgetInner.tsx +0 -83
  92. package/src/dashboard/DashboardCanvas.tsx +0 -210
  93. package/src/dashboard/DashboardCustomizeMenu.tsx +0 -53
  94. package/src/dashboard/DashboardGrid.tsx +0 -35
  95. package/src/dashboard/DashboardRange.tsx +0 -62
  96. package/src/dashboard/GoldCard.tsx +0 -38
  97. package/src/dashboard/KpiCard.tsx +0 -238
  98. package/src/dashboard/OnboardingWidget.tsx +0 -140
  99. package/src/dashboard/TableWidget.tsx +0 -51
  100. package/src/dashboard/define.ts +0 -53
  101. package/src/dashboard/icon.ts +0 -13
  102. package/src/dashboard/index.ts +0 -38
  103. package/src/dashboard/layout.ts +0 -50
  104. package/src/dashboard/preferences.ts +0 -71
  105. package/src/dashboard/types.ts +0 -41
  106. package/src/icons.ts +0 -66
  107. package/src/index.ts +0 -238
  108. package/src/layout/AppShell.tsx +0 -987
  109. package/src/layout/ContentSplit.tsx +0 -153
  110. package/src/layout/ModulePage.tsx +0 -466
  111. package/src/layout/PageTransition.tsx +0 -57
  112. package/src/layout/SaveBar.tsx +0 -247
  113. package/src/layout/Sidebar.tsx +0 -535
  114. package/src/layout/Topbar.tsx +0 -462
  115. package/src/layout/index.ts +0 -39
  116. package/src/layout/useCrossModuleBack.ts +0 -30
  117. package/src/primitives/Avatar.tsx +0 -48
  118. package/src/primitives/SearchCombobox.tsx +0 -216
  119. package/src/primitives/badge.tsx +0 -68
  120. package/src/primitives/breadcrumb.tsx +0 -43
  121. package/src/primitives/button.tsx +0 -60
  122. package/src/primitives/card.tsx +0 -79
  123. package/src/primitives/checkbox.tsx +0 -53
  124. package/src/primitives/confirm-dialog.tsx +0 -125
  125. package/src/primitives/currency-input.tsx +0 -121
  126. package/src/primitives/data-table.tsx +0 -160
  127. package/src/primitives/date-picker.tsx +0 -225
  128. package/src/primitives/dropdown.tsx +0 -196
  129. package/src/primitives/index.ts +0 -87
  130. package/src/primitives/input.tsx +0 -50
  131. package/src/primitives/list-view.tsx +0 -252
  132. package/src/primitives/markdown-editor.tsx +0 -75
  133. package/src/primitives/markdown.tsx +0 -169
  134. package/src/primitives/modal.tsx +0 -156
  135. package/src/primitives/phone-link.tsx +0 -138
  136. package/src/primitives/popover.tsx +0 -30
  137. package/src/primitives/search-select.tsx +0 -314
  138. package/src/primitives/segmented-control.tsx +0 -66
  139. package/src/primitives/select.tsx +0 -157
  140. package/src/primitives/separator.tsx +0 -29
  141. package/src/primitives/sheet.tsx +0 -181
  142. package/src/primitives/skeleton.tsx +0 -46
  143. package/src/primitives/tabs.tsx +0 -53
  144. package/src/primitives/time-picker.tsx +0 -148
  145. package/src/primitives/toast.tsx +0 -46
  146. package/src/primitives/tooltip.tsx +0 -28
  147. package/src/stores/index.ts +0 -10
  148. package/src/stores/layout.store.ts +0 -31
  149. package/src/stores/theme.store.ts +0 -457
  150. package/src/tailwind.ts +0 -43
  151. package/src/theme/index.ts +0 -94
  152. package/src/theme/preset.ts +0 -209
  153. package/src/theme/presets.ts +0 -106
  154. package/src/utils/cn.ts +0 -6
  155. /package/dist/{utils → lib}/cn.d.ts +0 -0
  156. /package/{src → dist}/styles.css +0 -0
@@ -1,169 +0,0 @@
1
- import * as React from 'react'
2
- import { cn } from '../utils/cn'
3
-
4
- /**
5
- * Minimal markdown renderer — builds React elements directly (no HTML string,
6
- * no dangerouslySetInnerHTML), so untrusted content is safe by construction.
7
- * Supports the subset content docs actually use: h1–h3, ---, blockquote,
8
- * ul/ol, paragraphs, and inline **bold** / *italic* / `code`.
9
- */
10
-
11
- const INLINE_TOKEN = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g
12
-
13
- function renderInline(text: string, keyPrefix: string): React.ReactNode[] {
14
- return text.split(INLINE_TOKEN).map((part, i) => {
15
- const key = `${keyPrefix}-${i}`
16
- if (part.startsWith('**') && part.endsWith('**') && part.length > 4) {
17
- return <strong key={key}>{part.slice(2, -2)}</strong>
18
- }
19
- if (part.startsWith('*') && part.endsWith('*') && part.length > 2) {
20
- return <em key={key}>{part.slice(1, -1)}</em>
21
- }
22
- if (part.startsWith('`') && part.endsWith('`') && part.length > 2) {
23
- return (
24
- <code key={key} className="rounded bg-muted px-1 py-0.5 font-mono text-[0.85em]">
25
- {part.slice(1, -1)}
26
- </code>
27
- )
28
- }
29
- return part === '' ? null : <React.Fragment key={key}>{part}</React.Fragment>
30
- })
31
- }
32
-
33
- type Block =
34
- | { type: 'heading'; level: 1 | 2 | 3; text: string }
35
- | { type: 'hr' }
36
- | { type: 'blockquote'; lines: string[] }
37
- | { type: 'ul'; items: string[] }
38
- | { type: 'ol'; items: string[] }
39
- | { type: 'p'; lines: string[] }
40
-
41
- function parseBlocks(source: string): Block[] {
42
- const lines = source.replace(/\r\n/g, '\n').split('\n')
43
- const blocks: Block[] = []
44
- let i = 0
45
- while (i < lines.length) {
46
- const line = lines[i]
47
- const trimmed = line.trim()
48
- if (trimmed === '') { i++; continue }
49
- const heading = /^(#{1,3})\s+(.*)$/.exec(trimmed)
50
- if (heading) {
51
- blocks.push({ type: 'heading', level: heading[1].length as 1 | 2 | 3, text: heading[2] })
52
- i++
53
- continue
54
- }
55
- if (/^(-{3,}|\*{3,})$/.test(trimmed)) {
56
- blocks.push({ type: 'hr' })
57
- i++
58
- continue
59
- }
60
- if (trimmed.startsWith('>')) {
61
- const quote: string[] = []
62
- while (i < lines.length && lines[i].trim().startsWith('>')) {
63
- quote.push(lines[i].trim().replace(/^>\s?/, ''))
64
- i++
65
- }
66
- blocks.push({ type: 'blockquote', lines: quote })
67
- continue
68
- }
69
- if (/^[-*]\s+/.test(trimmed)) {
70
- const items: string[] = []
71
- while (i < lines.length && /^[-*]\s+/.test(lines[i].trim())) {
72
- items.push(lines[i].trim().replace(/^[-*]\s+/, ''))
73
- i++
74
- }
75
- blocks.push({ type: 'ul', items })
76
- continue
77
- }
78
- if (/^\d+[.)]\s+/.test(trimmed)) {
79
- const items: string[] = []
80
- while (i < lines.length && /^\d+[.)]\s+/.test(lines[i].trim())) {
81
- items.push(lines[i].trim().replace(/^\d+[.)]\s+/, ''))
82
- i++
83
- }
84
- blocks.push({ type: 'ol', items })
85
- continue
86
- }
87
- const para: string[] = []
88
- while (i < lines.length && lines[i].trim() !== '' && !/^(#{1,3}\s|>|[-*]\s|\d+[.)]\s|-{3,}$|\*{3,}$)/.test(lines[i].trim())) {
89
- para.push(lines[i].trim())
90
- i++
91
- }
92
- blocks.push({ type: 'p', lines: para })
93
- }
94
- return blocks
95
- }
96
-
97
- const HEADING_CLASS: Record<1 | 2 | 3, string> = {
98
- 1: 'text-xl font-semibold tracking-tight',
99
- 2: 'text-base font-semibold tracking-tight',
100
- 3: 'text-sm font-semibold',
101
- }
102
-
103
- export function renderMarkdown(source: string): React.ReactNode {
104
- const blocks = parseBlocks(source)
105
- return blocks.map((block, bi) => {
106
- const key = `md-${bi}`
107
- switch (block.type) {
108
- case 'heading': {
109
- const Tag = (`h${block.level}`) as 'h1' | 'h2' | 'h3'
110
- return (
111
- <Tag key={key} className={HEADING_CLASS[block.level]}>
112
- {renderInline(block.text, key)}
113
- </Tag>
114
- )
115
- }
116
- case 'hr':
117
- return <hr key={key} className="border-border" />
118
- case 'blockquote':
119
- return (
120
- <blockquote key={key} className="border-l-2 border-border pl-3 text-muted-foreground">
121
- {block.lines.map((l, li) => (
122
- <p key={`${key}-${li}`}>{renderInline(l, `${key}-${li}`)}</p>
123
- ))}
124
- </blockquote>
125
- )
126
- case 'ul':
127
- return (
128
- <ul key={key} className="list-disc space-y-1 pl-5">
129
- {block.items.map((item, li) => (
130
- <li key={`${key}-${li}`}>{renderInline(item, `${key}-${li}`)}</li>
131
- ))}
132
- </ul>
133
- )
134
- case 'ol':
135
- return (
136
- <ol key={key} className="list-decimal space-y-1 pl-5">
137
- {block.items.map((item, li) => (
138
- <li key={`${key}-${li}`}>{renderInline(item, `${key}-${li}`)}</li>
139
- ))}
140
- </ol>
141
- )
142
- case 'p':
143
- return (
144
- <p key={key}>
145
- {block.lines.map((l, li) => (
146
- <React.Fragment key={`${key}-${li}`}>
147
- {li > 0 && <br />}
148
- {renderInline(l, `${key}-${li}`)}
149
- </React.Fragment>
150
- ))}
151
- </p>
152
- )
153
- }
154
- })
155
- }
156
-
157
- export interface MarkdownProps {
158
- source: string
159
- className?: string
160
- }
161
-
162
- /** Rendered markdown block — spacing tuned for script/brief documents. */
163
- export function Markdown({ source, className }: MarkdownProps) {
164
- return (
165
- <div className={cn('space-y-3 text-sm leading-relaxed text-foreground', className)}>
166
- {renderMarkdown(source)}
167
- </div>
168
- )
169
- }
@@ -1,156 +0,0 @@
1
- import * as React from 'react'
2
- import * as DialogPrimitive from '@radix-ui/react-dialog'
3
- import { X } from 'lucide-react'
4
-
5
- import { cn } from '../utils/cn'
6
-
7
- // ---------------------------------------------------------------------------
8
- // Animations are defined in styles.css under .saas-mdl-ov / .saas-mdl-ct
9
- // selectors with [data-state="open"/"closed"]. Radix defers unmount when
10
- // it detects animationName on the element — CSS must be loaded before render.
11
- // ---------------------------------------------------------------------------
12
-
13
- // ---------------------------------------------------------------------------
14
- // Sizes
15
- // ---------------------------------------------------------------------------
16
-
17
- const SIZE_MAP = {
18
- sm: 'sm:max-w-sm',
19
- md: 'sm:max-w-md',
20
- lg: 'sm:max-w-lg',
21
- xl: 'sm:max-w-xl',
22
- '2xl': 'sm:max-w-2xl',
23
- '3xl': 'sm:max-w-3xl',
24
- full: 'sm:max-w-[calc(100vw-4rem)]',
25
- } as const
26
-
27
- type ModalSize = keyof typeof SIZE_MAP
28
-
29
- // ---------------------------------------------------------------------------
30
- // Primitives re-export
31
- // ---------------------------------------------------------------------------
32
-
33
- const Modal = DialogPrimitive.Root
34
- const ModalTrigger = DialogPrimitive.Trigger
35
- const ModalClose = DialogPrimitive.Close
36
- const ModalPortal = DialogPrimitive.Portal
37
-
38
- // ---------------------------------------------------------------------------
39
- // Overlay — also serves as the flex centering wrapper
40
- // ---------------------------------------------------------------------------
41
-
42
- const ModalOverlay = React.forwardRef<
43
- React.ElementRef<typeof DialogPrimitive.Overlay>,
44
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
45
- >(({ className, ...props }, ref) => (
46
- <DialogPrimitive.Overlay
47
- ref={ref}
48
- className={cn(
49
- 'fayz-modal-overlay saas-mdl-ov fixed inset-0 z-50',
50
- 'flex items-center justify-center p-0 sm:p-6',
51
- className,
52
- )}
53
- {...props}
54
- />
55
- ))
56
- ModalOverlay.displayName = 'ModalOverlay'
57
-
58
- // ---------------------------------------------------------------------------
59
- // Content — rendered INSIDE the overlay (flex-centered)
60
- // ---------------------------------------------------------------------------
61
-
62
- const ModalContent = React.forwardRef<
63
- React.ElementRef<typeof DialogPrimitive.Content>,
64
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
65
- size?: ModalSize
66
- hideClose?: boolean
67
- noPadding?: boolean
68
- }
69
- >(({ className, children, size = 'lg', hideClose, noPadding, onPointerDownOutside, onInteractOutside, onCloseAutoFocus, ...props }, ref) => (
70
- <ModalPortal>
71
- <ModalOverlay>
72
- <DialogPrimitive.Content
73
- ref={ref}
74
- onCloseAutoFocus={(e) => {
75
- // Prevent Radix from stealing focus back to the trigger element on close.
76
- // This interferes with FullCalendar's selection and other interactive components.
77
- e.preventDefault()
78
- onCloseAutoFocus?.(e)
79
- }}
80
- onPointerDownOutside={(e) => {
81
- // Prevent Radix from intercepting clicks on portaled dropdowns (DatePicker, TimePicker, etc.)
82
- const target = e.target as HTMLElement
83
- if (target.closest('[data-modal-passthrough]') || target.closest('[data-radix-popper-content-wrapper]')) {
84
- e.preventDefault()
85
- }
86
- onPointerDownOutside?.(e)
87
- }}
88
- onInteractOutside={(e) => {
89
- const target = e.target as HTMLElement
90
- if (target.closest('[data-modal-passthrough]') || target.closest('[data-radix-popper-content-wrapper]')) {
91
- e.preventDefault()
92
- }
93
- onInteractOutside?.(e)
94
- }}
95
- className={cn(
96
- 'fayz-glass-surface fayz-modal-content saas-mdl-ct relative z-50 flex flex-col w-full outline-none',
97
- // Mobile fullscreen
98
- 'h-full rounded-none border-0',
99
- // Desktop centered card
100
- 'sm:h-auto sm:max-h-[85vh] sm:rounded-2xl sm:border',
101
- SIZE_MAP[size],
102
- noPadding ? '' : 'p-6 gap-4',
103
- className,
104
- )}
105
- {...props}
106
- >
107
- {children}
108
- {!hideClose && (
109
- <DialogPrimitive.Close className="absolute right-4 top-4 z-10 flex h-7 w-7 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring">
110
- <X className="h-4 w-4" />
111
- <span className="sr-only">Close</span>
112
- </DialogPrimitive.Close>
113
- )}
114
- </DialogPrimitive.Content>
115
- </ModalOverlay>
116
- </ModalPortal>
117
- ))
118
- ModalContent.displayName = 'ModalContent'
119
-
120
- // ---------------------------------------------------------------------------
121
- // Sections
122
- // ---------------------------------------------------------------------------
123
-
124
- function ModalHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
125
- return <div className={cn('flex flex-col space-y-1.5 text-center sm:text-left shrink-0', className)} {...props} />
126
- }
127
-
128
- function ModalBody({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
129
- return <div className={cn('flex-1 overflow-y-auto min-h-0', className)} {...props} />
130
- }
131
-
132
- function ModalFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
133
- return <div className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 shrink-0', className)} {...props} />
134
- }
135
-
136
- const ModalTitle = React.forwardRef<
137
- React.ElementRef<typeof DialogPrimitive.Title>,
138
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
139
- >(({ className, ...props }, ref) => (
140
- <DialogPrimitive.Title ref={ref} className={cn('text-lg font-semibold leading-none tracking-tight', className)} {...props} />
141
- ))
142
- ModalTitle.displayName = 'ModalTitle'
143
-
144
- const ModalDescription = React.forwardRef<
145
- React.ElementRef<typeof DialogPrimitive.Description>,
146
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
147
- >(({ className, ...props }, ref) => (
148
- <DialogPrimitive.Description ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
149
- ))
150
- ModalDescription.displayName = 'ModalDescription'
151
-
152
- export {
153
- Modal, ModalPortal, ModalOverlay, ModalClose, ModalTrigger,
154
- ModalContent, ModalHeader, ModalBody, ModalFooter, ModalTitle, ModalDescription,
155
- }
156
- export type { ModalSize }
@@ -1,138 +0,0 @@
1
- import * as React from 'react'
2
-
3
- import { cn } from '../utils/cn'
4
-
5
- // ---------------------------------------------------------------------------
6
- // Um telefone que abre a conversa, não o discador.
7
- //
8
- // `tel:` era a escolha óbvia e é a errada para este público: num desktop ele
9
- // não faz nada (ou abre um app que ninguém configurou), e no Brasil o primeiro
10
- // contato com um lead acontece no WhatsApp de qualquer jeito. O clique custava
11
- // um copiar-colar toda vez.
12
- //
13
- // Componente em vez de `<a>` solto porque a parte difícil não é o link — é
14
- // decidir o que fazer com o que está gravado. O telefone chega de formulário
15
- // público, de importação, de digitação: com DDI, sem DDI, com máscara, com
16
- // ramal. wa.me só aceita dígitos com DDI, e um link errado leva a pessoa a uma
17
- // conversa com o número errado — pior do que não linkar.
18
- // ---------------------------------------------------------------------------
19
-
20
- /** DDI assumido quando o número não traz um. Brasil, que é o padrão da stack. */
21
- const DEFAULT_COUNTRY_CODE = '55'
22
-
23
- /** Menor coisa discável: DDD + 8 dígitos. Abaixo disso não é telefone. */
24
- const MIN_NATIONAL_DIGITS = 10
25
-
26
- /** E.164 vai até 15; acima disso é lixo, ramal colado ou dois números juntos. */
27
- const MAX_DIGITS = 15
28
-
29
- export interface NormalizedPhone {
30
- /** Só dígitos, com DDI — o que o wa.me aceita. Null quando não dá para usar. */
31
- e164: string | null
32
- /** Como mostrar: formatado quando reconhecível, cru quando não. */
33
- display: string
34
- }
35
-
36
- /**
37
- * Resolve o que está gravado para algo que o wa.me aceita.
38
- *
39
- * Conservador de propósito: na dúvida devolve `e164: null`, e o componente
40
- * mostra texto simples. Deixar de linkar é um incômodo; linkar para o número
41
- * de outra pessoa é um erro que ninguém percebe até a conversa começar.
42
- */
43
- export function normalizePhone(
44
- raw: string | null | undefined,
45
- countryCode: string = DEFAULT_COUNTRY_CODE,
46
- ): NormalizedPhone {
47
- const input = (raw ?? '').trim()
48
- if (!input) return { e164: null, display: '' }
49
-
50
- const digits = input.replace(/\D/g, '')
51
- if (!digits || digits.length > MAX_DIGITS) {
52
- return { e164: null, display: input }
53
- }
54
-
55
- // `+` na origem significa que quem gravou já disse o país. Respeitar.
56
- const explicit = input.startsWith('+')
57
- let e164: string | null = null
58
-
59
- if (explicit) {
60
- e164 = digits
61
- } else if (digits.startsWith(countryCode) && digits.length > MIN_NATIONAL_DIGITS) {
62
- // Já vem com o DDI (o formulário público grava assim).
63
- e164 = digits
64
- } else if (digits.length >= MIN_NATIONAL_DIGITS) {
65
- // Número nacional: o DDI é o que falta.
66
- e164 = `${countryCode}${digits}`
67
- }
68
-
69
- if (e164 && (e164.length < MIN_NATIONAL_DIGITS + 1 || e164.length > MAX_DIGITS)) {
70
- e164 = null
71
- }
72
-
73
- return { e164, display: formatPhone(e164 ?? digits, input, countryCode) }
74
- }
75
-
76
- /** "5521999998888" → "+55 (21) 99999-8888". Fora do Brasil, devolve o original. */
77
- function formatPhone(digits: string, fallback: string, countryCode: string): string {
78
- if (countryCode !== '55' || !digits.startsWith('55')) return fallback
79
- const national = digits.slice(2)
80
- // 11 = celular (9 dígitos), 10 = fixo.
81
- if (national.length !== 10 && national.length !== 11) return fallback
82
- const ddd = national.slice(0, 2)
83
- const rest = national.slice(2)
84
- const head = rest.length === 9 ? rest.slice(0, 5) : rest.slice(0, 4)
85
- const tail = rest.length === 9 ? rest.slice(5) : rest.slice(4)
86
- return `+55 (${ddd}) ${head}-${tail}`
87
- }
88
-
89
- export interface PhoneLinkProps extends Omit<React.ComponentPropsWithoutRef<'a'>, 'href'> {
90
- /** O telefone como está gravado — com ou sem DDI, com ou sem máscara. */
91
- phone: string | null | undefined
92
- /** Texto que já vai digitado na conversa. */
93
- message?: string
94
- /** DDI assumido quando o número não traz um. Default: 55. */
95
- countryCode?: string
96
- /** Mostra o número cru em vez do formatado. */
97
- raw?: boolean
98
- /** Renderizado no lugar do número (um ícone + label, por exemplo). */
99
- children?: React.ReactNode
100
- }
101
-
102
- /**
103
- * Telefone clicável que abre o WhatsApp em outra aba.
104
- *
105
- * Sem número utilizável, vira `<span>`: um link morto convida ao clique e não
106
- * entrega nada.
107
- */
108
- export const PhoneLink = React.forwardRef<HTMLAnchorElement, PhoneLinkProps>(
109
- ({ phone, message, countryCode, raw, className, children, ...props }, ref) => {
110
- const { e164, display } = normalizePhone(phone, countryCode)
111
- const label = children ?? (raw ? (phone ?? '') : display)
112
-
113
- if (!e164) {
114
- return label ? <span className={className}>{label}</span> : null
115
- }
116
-
117
- const href = message
118
- ? `https://wa.me/${e164}?text=${encodeURIComponent(message)}`
119
- : `https://wa.me/${e164}`
120
-
121
- return (
122
- <a
123
- ref={ref}
124
- href={href}
125
- target="_blank"
126
- // noreferrer junto: wa.me abre em aba nova, e sem isto a página de
127
- // destino recebe uma referência para a janela de origem.
128
- rel="noopener noreferrer"
129
- title={`WhatsApp ${display}`}
130
- className={cn('hover:underline', className)}
131
- {...props}
132
- >
133
- {label}
134
- </a>
135
- )
136
- },
137
- )
138
- PhoneLink.displayName = 'PhoneLink'
@@ -1,30 +0,0 @@
1
- import * as React from 'react'
2
- import * as PopoverPrimitive from '@radix-ui/react-popover'
3
-
4
- import { cn } from '../utils/cn'
5
-
6
- const Popover = PopoverPrimitive.Root
7
- const PopoverTrigger = PopoverPrimitive.Trigger
8
- const PopoverAnchor = PopoverPrimitive.Anchor
9
-
10
- const PopoverContent = React.forwardRef<
11
- React.ElementRef<typeof PopoverPrimitive.Content>,
12
- React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
13
- >(({ className, align = 'start', sideOffset = 4, ...props }, ref) => (
14
- <PopoverPrimitive.Portal>
15
- <PopoverPrimitive.Content
16
- ref={ref}
17
- align={align}
18
- sideOffset={sideOffset}
19
- className={cn(
20
- 'fayz-glass-surface z-[110] w-72 rounded-xl border bg-popover p-0 text-popover-foreground shadow-lg outline-none',
21
- 'data-[state=open]:animate-zoom-in data-[state=closed]:animate-zoom-out',
22
- className
23
- )}
24
- {...props}
25
- />
26
- </PopoverPrimitive.Portal>
27
- ))
28
- PopoverContent.displayName = PopoverPrimitive.Content.displayName
29
-
30
- export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent }