@fayz-ai/ui 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/analytics/AnalyticsCard.d.ts +10 -0
- package/dist/analytics/AnalyticsCard.d.ts.map +1 -0
- package/dist/analytics/AnalyticsControls.d.ts +18 -0
- package/dist/analytics/AnalyticsControls.d.ts.map +1 -0
- package/dist/analytics/AnalyticsGrid.d.ts +16 -0
- package/dist/analytics/AnalyticsGrid.d.ts.map +1 -0
- package/dist/analytics/context.d.ts +58 -0
- package/dist/analytics/context.d.ts.map +1 -0
- package/dist/analytics/derive.d.ts +81 -0
- package/dist/analytics/derive.d.ts.map +1 -0
- package/dist/analytics/index.d.ts +13 -0
- package/dist/analytics/index.d.ts.map +1 -0
- package/dist/analytics/range.d.ts +22 -0
- package/dist/analytics/range.d.ts.map +1 -0
- package/dist/{chunk-PVIB724L.js → chunk-22EVFMS3.js} +5 -3
- package/dist/chunk-22EVFMS3.js.map +1 -0
- package/dist/{chunk-G6OKZ7WL.js → chunk-6V5MGYXH.js} +13 -9
- package/dist/chunk-6V5MGYXH.js.map +1 -0
- package/dist/{chunk-KV3MHB6N.js → chunk-H63TOSQ7.js} +196 -53
- package/dist/chunk-H63TOSQ7.js.map +1 -0
- package/dist/{chunk-OUPJDGKE.js → chunk-I6CPKAOD.js} +134 -37
- package/dist/chunk-I6CPKAOD.js.map +1 -0
- package/dist/{chunk-YRVE4FOD.js → chunk-OKZ6WQB3.js} +3 -3
- package/dist/chunk-OKZ6WQB3.js.map +1 -0
- package/dist/{chunk-S5KNM4D2.js → chunk-SEIY2F5F.js} +235 -107
- package/dist/chunk-SEIY2F5F.js.map +1 -0
- package/dist/{chunk-75EFDLST.js → chunk-VAKWUUVC.js} +157 -6
- package/dist/chunk-VAKWUUVC.js.map +1 -0
- package/dist/dashboard/OnboardingWidget.d.ts +2 -14
- package/dist/dashboard/OnboardingWidget.d.ts.map +1 -1
- package/dist/dashboard/SidebarOnboarding.d.ts +28 -0
- package/dist/dashboard/SidebarOnboarding.d.ts.map +1 -0
- package/dist/dashboard/index.d.ts +2 -0
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +4 -4
- package/dist/dashboard/types.d.ts +18 -0
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/dashboard/useOnboardingProgress.d.ts +28 -0
- package/dist/dashboard/useOnboardingProgress.d.ts.map +1 -0
- package/dist/icons.js +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +960 -14
- package/dist/index.js.map +1 -1
- package/dist/layout/AppShell.d.ts +14 -1
- package/dist/layout/AppShell.d.ts.map +1 -1
- package/dist/layout/ModulePage.d.ts +11 -2
- package/dist/layout/ModulePage.d.ts.map +1 -1
- package/dist/layout/Sidebar.d.ts +8 -1
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/index.js +4 -4
- package/dist/lib/cn.d.ts.map +1 -0
- package/dist/preset.d.ts +3 -0
- package/dist/preset.d.ts.map +1 -0
- package/dist/primitives/data-table.d.ts +19 -4
- package/dist/primitives/data-table.d.ts.map +1 -1
- package/dist/primitives/date-picker.d.ts +4 -2
- package/dist/primitives/date-picker.d.ts.map +1 -1
- package/dist/primitives/filter-chip.d.ts +26 -0
- package/dist/primitives/filter-chip.d.ts.map +1 -0
- package/dist/primitives/header-tabs.d.ts +32 -0
- package/dist/primitives/header-tabs.d.ts.map +1 -0
- package/dist/primitives/hint.d.ts +25 -0
- package/dist/primitives/hint.d.ts.map +1 -0
- package/dist/primitives/index.d.ts +7 -0
- package/dist/primitives/index.d.ts.map +1 -1
- package/dist/primitives/index.js +4 -4
- package/dist/primitives/list-view.d.ts +14 -1
- package/dist/primitives/list-view.d.ts.map +1 -1
- package/dist/primitives/page-header.d.ts +23 -0
- package/dist/primitives/page-header.d.ts.map +1 -0
- package/dist/primitives/stat-tiles.d.ts +30 -0
- package/dist/primitives/stat-tiles.d.ts.map +1 -0
- package/dist/primitives/switch.d.ts +11 -0
- package/dist/primitives/switch.d.ts.map +1 -0
- package/dist/stores/index.d.ts +1 -0
- package/dist/stores/index.d.ts.map +1 -1
- package/dist/stores/page-header.store.d.ts +69 -0
- package/dist/stores/page-header.store.d.ts.map +1 -0
- package/package.json +4 -13
- package/dist/chunk-75EFDLST.js.map +0 -1
- package/dist/chunk-G6OKZ7WL.js.map +0 -1
- package/dist/chunk-KV3MHB6N.js.map +0 -1
- package/dist/chunk-OUPJDGKE.js.map +0 -1
- package/dist/chunk-PVIB724L.js.map +0 -1
- package/dist/chunk-S5KNM4D2.js.map +0 -1
- package/dist/chunk-YRVE4FOD.js.map +0 -1
- package/dist/utils/cn.d.ts.map +0 -1
- package/src/dashboard/ChartWidget.tsx +0 -50
- package/src/dashboard/ChartWidgetInner.tsx +0 -83
- package/src/dashboard/DashboardCanvas.tsx +0 -210
- package/src/dashboard/DashboardCustomizeMenu.tsx +0 -53
- package/src/dashboard/DashboardGrid.tsx +0 -35
- package/src/dashboard/DashboardRange.tsx +0 -62
- package/src/dashboard/GoldCard.tsx +0 -38
- package/src/dashboard/KpiCard.tsx +0 -238
- package/src/dashboard/OnboardingWidget.tsx +0 -140
- package/src/dashboard/TableWidget.tsx +0 -51
- package/src/dashboard/define.ts +0 -53
- package/src/dashboard/icon.ts +0 -13
- package/src/dashboard/index.ts +0 -38
- package/src/dashboard/layout.ts +0 -50
- package/src/dashboard/preferences.ts +0 -71
- package/src/dashboard/types.ts +0 -41
- package/src/icons.ts +0 -66
- package/src/index.ts +0 -238
- package/src/layout/AppShell.tsx +0 -987
- package/src/layout/ContentSplit.tsx +0 -153
- package/src/layout/ModulePage.tsx +0 -466
- package/src/layout/PageTransition.tsx +0 -57
- package/src/layout/SaveBar.tsx +0 -247
- package/src/layout/Sidebar.tsx +0 -535
- package/src/layout/Topbar.tsx +0 -462
- package/src/layout/index.ts +0 -39
- package/src/layout/useCrossModuleBack.ts +0 -30
- package/src/primitives/Avatar.tsx +0 -48
- package/src/primitives/SearchCombobox.tsx +0 -216
- package/src/primitives/badge.tsx +0 -68
- package/src/primitives/breadcrumb.tsx +0 -43
- package/src/primitives/button.tsx +0 -60
- package/src/primitives/card.tsx +0 -79
- package/src/primitives/checkbox.tsx +0 -53
- package/src/primitives/confirm-dialog.tsx +0 -125
- package/src/primitives/currency-input.tsx +0 -121
- package/src/primitives/data-table.tsx +0 -160
- package/src/primitives/date-picker.tsx +0 -225
- package/src/primitives/dropdown.tsx +0 -196
- package/src/primitives/index.ts +0 -87
- package/src/primitives/input.tsx +0 -50
- package/src/primitives/list-view.tsx +0 -252
- package/src/primitives/markdown-editor.tsx +0 -75
- package/src/primitives/markdown.tsx +0 -169
- package/src/primitives/modal.tsx +0 -156
- package/src/primitives/phone-link.tsx +0 -138
- package/src/primitives/popover.tsx +0 -30
- package/src/primitives/search-select.tsx +0 -314
- package/src/primitives/segmented-control.tsx +0 -66
- package/src/primitives/select.tsx +0 -157
- package/src/primitives/separator.tsx +0 -29
- package/src/primitives/sheet.tsx +0 -181
- package/src/primitives/skeleton.tsx +0 -46
- package/src/primitives/tabs.tsx +0 -53
- package/src/primitives/time-picker.tsx +0 -148
- package/src/primitives/toast.tsx +0 -46
- package/src/primitives/tooltip.tsx +0 -28
- package/src/stores/index.ts +0 -10
- package/src/stores/layout.store.ts +0 -31
- package/src/stores/theme.store.ts +0 -457
- package/src/styles.css +0 -461
- package/src/tailwind.ts +0 -43
- package/src/theme/index.ts +0 -94
- package/src/theme/preset.ts +0 -209
- package/src/theme/presets.ts +0 -106
- package/src/utils/cn.ts +0 -6
- /package/dist/{utils → lib}/cn.d.ts +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
|
-
}
|
package/src/primitives/modal.tsx
DELETED
|
@@ -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 }
|