@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,121 +0,0 @@
1
- import * as React from 'react'
2
- import { cn } from '../utils/cn'
3
-
4
- interface CurrencyInputProps {
5
- value: number
6
- onChange: (value: number) => void
7
- symbol?: string
8
- locale?: string
9
- currencyCode?: string
10
- label?: string
11
- placeholder?: string
12
- className?: string
13
- disabled?: boolean
14
- }
15
-
16
- /**
17
- * Currency input with automatic decimal formatting.
18
- *
19
- * - Always displays the value with 2 decimal places using the project's locale
20
- * - Input works like an ATM: digits push from right, decimals are fixed
21
- * - Typing "12345" shows "123,45" (or "123.45" depending on locale)
22
- * - Backspace removes the last digit
23
- * - Supports the project currency symbol
24
- */
25
- export function CurrencyInput({
26
- value,
27
- onChange,
28
- symbol = 'R$',
29
- locale = 'pt-BR',
30
- currencyCode = 'BRL',
31
- label,
32
- placeholder,
33
- className,
34
- disabled,
35
- }: CurrencyInputProps) {
36
- const inputRef = React.useRef<HTMLInputElement>(null)
37
- const [focused, setFocused] = React.useState(false)
38
-
39
- // Format number to locale string with 2 decimals
40
- const format = React.useCallback((v: number) => {
41
- return new Intl.NumberFormat(locale, {
42
- minimumFractionDigits: 2,
43
- maximumFractionDigits: 2,
44
- }).format(v)
45
- }, [locale])
46
-
47
- const displayValue = format(value)
48
-
49
- // Handle key-by-key input — ATM-style (digits push from right)
50
- function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
51
- if (disabled) return
52
-
53
- // Allow tab, arrow keys, etc.
54
- if (['Tab', 'Escape', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) return
55
-
56
- e.preventDefault()
57
-
58
- if (e.key === 'Backspace' || e.key === 'Delete') {
59
- // Remove last digit: 123.45 → 12.34
60
- const cents = Math.floor(Math.round(value * 100) / 10)
61
- onChange(cents / 100)
62
- return
63
- }
64
-
65
- // Only accept digits
66
- if (!/^\d$/.test(e.key)) return
67
-
68
- // Append digit: 12.34 + '5' → 123.45
69
- const currentCents = Math.round(value * 100)
70
- const newCents = currentCents * 10 + parseInt(e.key, 10)
71
- // Cap at 999,999,999.99
72
- if (newCents > 99999999999) return
73
- onChange(newCents / 100)
74
- }
75
-
76
- // Handle paste — parse number from pasted text
77
- function handlePaste(e: React.ClipboardEvent) {
78
- e.preventDefault()
79
- const text = e.clipboardData.getData('text')
80
- // Remove everything except digits, dots, commas
81
- const cleaned = text.replace(/[^\d.,]/g, '')
82
- // Replace comma with dot for parsing
83
- const normalized = cleaned.replace(',', '.')
84
- const parsed = parseFloat(normalized)
85
- if (!isNaN(parsed)) {
86
- onChange(Math.round(parsed * 100) / 100)
87
- }
88
- }
89
-
90
- return (
91
- <div className={className}>
92
- {label && <label className="text-xs font-medium text-muted-foreground">{label}</label>}
93
- <div
94
- className={cn(
95
- 'flex items-center rounded-lg border border-input bg-background ring-offset-background transition-colors cursor-text',
96
- label && 'mt-1',
97
- focused && 'border-ring ring-2 ring-ring ring-offset-2',
98
- disabled && 'opacity-50 cursor-not-allowed',
99
- )}
100
- onClick={() => inputRef.current?.focus()}
101
- >
102
- <span className="pl-3 pr-1 text-sm font-medium text-muted-foreground select-none">{symbol}</span>
103
- <input
104
- ref={inputRef}
105
- type="text"
106
- inputMode="numeric"
107
- value={displayValue}
108
- onKeyDown={handleKeyDown}
109
- onPaste={handlePaste}
110
- onFocus={() => setFocused(true)}
111
- onBlur={() => setFocused(false)}
112
- placeholder={placeholder ?? format(0)}
113
- disabled={disabled}
114
- readOnly={false}
115
- className="flex-1 bg-transparent px-2 py-2 text-sm outline-none text-right tabular-nums caret-primary selection:bg-primary/20"
116
- onChange={() => {}}
117
- />
118
- </div>
119
- </div>
120
- )
121
- }
@@ -1,160 +0,0 @@
1
- import * as React from 'react'
2
- import {
3
- type ColumnDef,
4
- type SortingState,
5
- flexRender,
6
- getCoreRowModel,
7
- getSortedRowModel,
8
- useReactTable,
9
- } from '@tanstack/react-table'
10
- import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'
11
-
12
- import { cn } from '../utils/cn'
13
-
14
- interface DataTableProps<TData, TValue> {
15
- columns: ColumnDef<TData, TValue>[]
16
- data: TData[]
17
- loading?: boolean
18
- skeletonRows?: number
19
- emptyMessage?: string
20
- onRowClick?: (row: TData) => void
21
- /** Visual variant: 'card' (white bg, card wrapper — default) or 'flat' (transparent bg) */
22
- variant?: 'card' | 'flat'
23
- /** Compact mode: smaller padding and font size */
24
- compact?: boolean
25
- }
26
-
27
- function DataTable<TData, TValue>({
28
- columns,
29
- data,
30
- loading = false,
31
- skeletonRows = 3,
32
- emptyMessage = 'No results.',
33
- onRowClick,
34
- variant = 'card',
35
- compact = false,
36
- }: DataTableProps<TData, TValue>) {
37
- const isCard = variant === 'card'
38
- // Rounded box and scroll container must be SEPARATE elements: a scroll
39
- // container paints its content over its own border-radius, which squared
40
- // the corners the moment a row background met them.
41
- const wrapperCls = isCard
42
- ? 'overflow-hidden rounded-lg border bg-card shadow-sm'
43
- : 'overflow-hidden rounded-lg border'
44
- const headerCls = isCard ? 'border-b bg-muted/50' : 'border-b bg-muted/30'
45
- const thCls = compact ? 'px-3 py-1.5 text-left text-[11px] font-medium text-muted-foreground' : 'px-4 py-2.5 text-left font-medium text-muted-foreground'
46
- const tdCls = compact ? 'px-3 py-1.5 align-middle text-xs' : 'px-4 py-3 align-middle'
47
- const tableCls = compact ? 'w-full text-xs min-w-max' : 'w-full text-sm min-w-max'
48
- const rowHoverCls = isCard ? 'hover:bg-muted/30' : 'hover:bg-muted/20'
49
- const [sorting, setSorting] = React.useState<SortingState>([])
50
-
51
- const table = useReactTable({
52
- data,
53
- columns,
54
- getCoreRowModel: getCoreRowModel(),
55
- getSortedRowModel: getSortedRowModel(),
56
- onSortingChange: setSorting,
57
- state: { sorting },
58
- })
59
-
60
- if (loading) {
61
- return (
62
- <div className={wrapperCls}>
63
- <div className="overflow-x-auto">
64
- <table className={tableCls}>
65
- <thead>
66
- <tr className={headerCls}>
67
- {columns.map((_, i) => (
68
- <th key={i} className={thCls}>
69
- <div className="h-3.5 w-20 animate-pulse rounded bg-muted" />
70
- </th>
71
- ))}
72
- </tr>
73
- </thead>
74
- <tbody>
75
- {Array.from({ length: skeletonRows }).map((_, i) => (
76
- <tr key={i} className="border-b last:border-0">
77
- {columns.map((_, j) => (
78
- <td key={j} className={tdCls}>
79
- <div className="h-3.5 w-full animate-pulse rounded bg-muted" />
80
- </td>
81
- ))}
82
- </tr>
83
- ))}
84
- </tbody>
85
- </table>
86
- </div>
87
- </div>
88
- )
89
- }
90
-
91
- return (
92
- <div className={wrapperCls}>
93
- <div className="overflow-x-auto">
94
- <table className={tableCls}>
95
- <thead>
96
- {table.getHeaderGroups().map((headerGroup) => (
97
- <tr key={headerGroup.id} className={headerCls}>
98
- {headerGroup.headers.map((header) => {
99
- const sorted = header.column.getIsSorted()
100
- return (
101
- <th
102
- key={header.id}
103
- className={thCls}
104
- >
105
- {header.isPlaceholder ? null : header.column.getCanSort() ? (
106
- <button
107
- className="flex items-center gap-1 hover:text-foreground transition-colors"
108
- onClick={header.column.getToggleSortingHandler()}
109
- >
110
- {flexRender(header.column.columnDef.header, header.getContext())}
111
- {sorted === 'asc' ? (
112
- <ArrowUp className="h-3 w-3" />
113
- ) : sorted === 'desc' ? (
114
- <ArrowDown className="h-3 w-3" />
115
- ) : (
116
- <ArrowUpDown className="h-3 w-3 opacity-40" />
117
- )}
118
- </button>
119
- ) : (
120
- flexRender(header.column.columnDef.header, header.getContext())
121
- )}
122
- </th>
123
- )
124
- })}
125
- </tr>
126
- ))}
127
- </thead>
128
- <tbody>
129
- {table.getRowModel().rows?.length ? (
130
- table.getRowModel().rows.map((row) => (
131
- <tr
132
- key={row.id}
133
- className={cn(
134
- `border-b last:border-0 transition-colors ${rowHoverCls}`,
135
- onRowClick && 'cursor-pointer'
136
- )}
137
- onClick={() => onRowClick?.(row.original)}
138
- >
139
- {row.getVisibleCells().map((cell) => (
140
- <td key={cell.id} className={tdCls}>
141
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
142
- </td>
143
- ))}
144
- </tr>
145
- ))
146
- ) : (
147
- <tr>
148
- <td colSpan={columns.length} className="h-24 text-center text-muted-foreground">
149
- {emptyMessage}
150
- </td>
151
- </tr>
152
- )}
153
- </tbody>
154
- </table>
155
- </div>
156
- </div>
157
- )
158
- }
159
-
160
- export { DataTable, type DataTableProps }
@@ -1,225 +0,0 @@
1
- import React, { useState, useEffect, useRef, useMemo } from 'react'
2
- import { createPortal } from 'react-dom'
3
- import { ChevronLeft, ChevronRight, Calendar } from 'lucide-react'
4
- import { useTranslation } from '@fayz-ai/core'
5
- import { getCurrentLocale } from '@fayz-ai/core'
6
-
7
- // ---------------------------------------------------------------------------
8
- // DatePicker — custom dropdown calendar, Google Calendar style
9
- // ---------------------------------------------------------------------------
10
-
11
- const DAY_LABELS: Record<string, string[]> = {
12
- en: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
13
- 'pt-BR': ['D', 'S', 'T', 'Q', 'Q', 'S', 'S'],
14
- }
15
-
16
- function toDateLocale(locale: string): string {
17
- if (locale === 'pt-BR') return 'pt-BR'
18
- return 'en-US'
19
- }
20
-
21
- function isSameDay(a: Date, b: Date) {
22
- return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
23
- }
24
-
25
- function isToday(d: Date) { return isSameDay(d, new Date()) }
26
-
27
- function formatDate(date: Date, locale: string) {
28
- return date.toLocaleDateString(toDateLocale(locale), { month: 'short', day: 'numeric', year: 'numeric' })
29
- }
30
-
31
- /** Toolbar label: the year is noise while you're inside it, and two of these sit
32
- * side by side in a range — it only earns its space on another year. */
33
- function formatDateCompact(date: Date, locale: string) {
34
- const sameYear = date.getFullYear() === new Date().getFullYear()
35
- return date.toLocaleDateString(toDateLocale(locale), {
36
- month: 'short',
37
- day: 'numeric',
38
- ...(sameYear ? {} : { year: 'numeric' }),
39
- })
40
- }
41
-
42
- function toDateStr(d: Date) {
43
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
44
- }
45
-
46
- function parseDate(s: string): Date {
47
- const [y, m, d] = s.split('-').map(Number)
48
- return new Date(y, m - 1, d)
49
- }
50
-
51
- interface DatePickerProps {
52
- /** Value as YYYY-MM-DD string */
53
- value: string
54
- onChange: (value: string) => void
55
- className?: string
56
- /**
57
- * `input` (default) is the form-field trigger. `toolbar` is the 26px pill that
58
- * matches SegmentedControl/Button `sm`, for filter bars where a full-height
59
- * form field towers over everything beside it.
60
- */
61
- variant?: 'input' | 'toolbar'
62
- /** Open the dropdown immediately on mount */
63
- defaultOpen?: boolean
64
- /** Called when the dropdown opens or closes */
65
- onOpenChange?: (open: boolean) => void
66
- }
67
-
68
- const TRIGGER_VARIANT: Record<'input' | 'toolbar', string> = {
69
- input: 'rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm w-full',
70
- toolbar: 'h-[26px] rounded-button border border-border bg-card shadow-button px-2.5 text-xs font-medium',
71
- }
72
-
73
- export function DatePicker({ value, onChange, className, variant = 'input', defaultOpen, onOpenChange }: DatePickerProps) {
74
- const t = useTranslation()
75
- const locale = getCurrentLocale()
76
- const [open, setOpen] = useState(defaultOpen ?? false)
77
- const ref = useRef<HTMLDivElement>(null)
78
- const triggerRef = useRef<HTMLButtonElement>(null)
79
- const dropdownRef = useRef<HTMLDivElement>(null)
80
- const selectedDate = value ? parseDate(value) : new Date()
81
- const [viewMonth, setViewMonth] = useState(new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1))
82
- const [pos, setPos] = useState<{ top: number; left: number }>({ top: 0, left: 0 })
83
-
84
- const dayLabels = DAY_LABELS[locale] ?? DAY_LABELS.en
85
- const dateLocale = toDateLocale(locale)
86
-
87
- // Position the portal dropdown relative to the trigger
88
- useEffect(() => {
89
- if (!open || !triggerRef.current) return
90
- const rect = triggerRef.current.getBoundingClientRect()
91
- const dropW = 260
92
- let left = rect.left
93
- if (left + dropW > window.innerWidth - 12) left = window.innerWidth - dropW - 12
94
- if (left < 8) left = 8
95
- setPos({ top: rect.bottom + 4, left })
96
- }, [open])
97
-
98
- useEffect(() => {
99
- if (!open) return
100
- const handler = (e: MouseEvent) => {
101
- const target = e.target as Node
102
- if (ref.current?.contains(target)) return
103
- if (dropdownRef.current?.contains(target)) return
104
- setOpen(false)
105
- onOpenChange?.(false)
106
- }
107
- document.addEventListener('mousedown', handler)
108
- return () => document.removeEventListener('mousedown', handler)
109
- }, [open, onOpenChange])
110
-
111
- // Sync view month when value changes externally
112
- useEffect(() => {
113
- if (value) {
114
- const d = parseDate(value)
115
- setViewMonth(new Date(d.getFullYear(), d.getMonth(), 1))
116
- }
117
- }, [value])
118
-
119
- const monthLabel = viewMonth.toLocaleDateString(dateLocale, { month: 'long', year: 'numeric' })
120
-
121
- const weeks = useMemo(() => {
122
- const year = viewMonth.getFullYear()
123
- const month = viewMonth.getMonth()
124
- const firstDay = new Date(year, month, 1)
125
- const startOffset = firstDay.getDay()
126
- const daysInMonth = new Date(year, month + 1, 0).getDate()
127
- const prevMonthDays = new Date(year, month, 0).getDate()
128
-
129
- const cells: { date: Date; inMonth: boolean }[] = []
130
- for (let i = startOffset - 1; i >= 0; i--) {
131
- cells.push({ date: new Date(year, month - 1, prevMonthDays - i), inMonth: false })
132
- }
133
- for (let d = 1; d <= daysInMonth; d++) {
134
- cells.push({ date: new Date(year, month, d), inMonth: true })
135
- }
136
- while (cells.length % 7 !== 0) {
137
- cells.push({ date: new Date(year, month + 1, cells.length - startOffset - daysInMonth + 1), inMonth: false })
138
- }
139
-
140
- const rows: typeof cells[] = []
141
- for (let i = 0; i < cells.length; i += 7) rows.push(cells.slice(i, i + 7))
142
- return rows
143
- }, [viewMonth])
144
-
145
- function handleSelect(date: Date) {
146
- onChange(toDateStr(date))
147
- setOpen(false)
148
- }
149
-
150
- return (
151
- <div ref={ref} className={`relative ${className ?? ''}`}>
152
- {/* Trigger */}
153
- <button ref={triggerRef} type="button" onClick={() => setOpen((p) => !p)}
154
- className={`flex items-center gap-2 text-left transition-colors hover:bg-muted/30 ${TRIGGER_VARIANT[variant]}`}>
155
- <Calendar className={`shrink-0 text-muted-foreground ${variant === 'toolbar' ? 'h-3 w-3' : 'h-3.5 w-3.5'}`} />
156
- <span className="whitespace-nowrap">
157
- {value
158
- ? (variant === 'toolbar' ? formatDateCompact : formatDate)(selectedDate, locale)
159
- : t('common.selectDate')}
160
- </span>
161
- </button>
162
-
163
- {/* Dropdown — portal to escape overflow containers */}
164
- {open && createPortal(
165
- <div ref={dropdownRef} data-modal-passthrough
166
- style={{ position: 'fixed', top: pos.top, left: pos.left, zIndex: 9999, pointerEvents: 'auto' }}
167
- className="fayz-glass-surface rounded-xl border bg-popover shadow-xl p-3 w-[260px]">
168
- {/* Month nav */}
169
- <div className="flex items-center justify-between mb-2">
170
- <span className="text-sm font-medium capitalize">{monthLabel}</span>
171
- <div className="flex items-center gap-0.5">
172
- <button type="button" onClick={() => setViewMonth(new Date(viewMonth.getFullYear(), viewMonth.getMonth() - 1, 1))}
173
- className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted transition-colors">
174
- <ChevronLeft className="h-4 w-4" />
175
- </button>
176
- <button type="button" onClick={() => setViewMonth(new Date(viewMonth.getFullYear(), viewMonth.getMonth() + 1, 1))}
177
- className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted transition-colors">
178
- <ChevronRight className="h-4 w-4" />
179
- </button>
180
- </div>
181
- </div>
182
-
183
- {/* Day labels */}
184
- <div className="grid grid-cols-7 mb-1">
185
- {dayLabels.map((l, i) => (
186
- <div key={i} className="flex h-8 items-center justify-center text-[11px] font-medium text-muted-foreground">{l}</div>
187
- ))}
188
- </div>
189
-
190
- {/* Dates */}
191
- {weeks.map((week, wi) => (
192
- <div key={wi} className="grid grid-cols-7">
193
- {week.map(({ date, inMonth }, di) => {
194
- const sel = value && isSameDay(date, selectedDate)
195
- const today = isToday(date)
196
- return (
197
- <button key={di} type="button"
198
- onClick={() => handleSelect(date)}
199
- className={`flex h-8 w-8 items-center justify-center rounded-full text-xs mx-auto transition-colors ${
200
- sel
201
- ? 'bg-primary text-primary-foreground font-semibold'
202
- : today
203
- ? 'bg-primary/10 text-primary font-semibold'
204
- : inMonth
205
- ? 'hover:bg-muted text-foreground'
206
- : 'text-muted-foreground/40 hover:bg-muted/50'
207
- }`}>
208
- {date.getDate()}
209
- </button>
210
- )
211
- })}
212
- </div>
213
- ))}
214
-
215
- {/* Today shortcut */}
216
- <div className="mt-2 pt-2 border-t">
217
- <button type="button" onClick={() => { handleSelect(new Date()) }}
218
- className="text-xs text-primary font-medium hover:underline">{t('agenda.calendar.today')}</button>
219
- </div>
220
- </div>,
221
- document.body,
222
- )}
223
- </div>
224
- )
225
- }