@fayz-ai/ui 0.6.0 → 0.7.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/dist/chunk-75EFDLST.js +274 -0
- package/dist/chunk-75EFDLST.js.map +1 -0
- package/dist/{chunk-YB5ZJKNG.cjs → chunk-7PTRAWHO.cjs} +263 -225
- package/dist/chunk-7PTRAWHO.cjs.map +1 -0
- package/dist/{chunk-CZ6X6PZT.js → chunk-COC5DDFG.js} +313 -44
- package/dist/chunk-COC5DDFG.js.map +1 -0
- package/dist/{chunk-MYZT2OG7.js → chunk-FY7OZNKU.js} +24 -68
- package/dist/chunk-FY7OZNKU.js.map +1 -0
- package/dist/chunk-H6OULCHF.cjs +81 -0
- package/dist/chunk-H6OULCHF.cjs.map +1 -0
- package/dist/{chunk-GE6XUREM.cjs → chunk-HG5RH3B3.cjs} +339 -68
- package/dist/chunk-HG5RH3B3.cjs.map +1 -0
- package/dist/{chunk-7TBDPNLY.cjs → chunk-NWPMNPGR.cjs} +76 -47
- package/dist/chunk-NWPMNPGR.cjs.map +1 -0
- package/dist/{chunk-D6SXPF4F.cjs → chunk-ODY3GZ3Y.cjs} +41 -87
- package/dist/chunk-ODY3GZ3Y.cjs.map +1 -0
- package/dist/{chunk-22J2RTMN.js → chunk-OTUH4K4B.js} +196 -150
- package/dist/chunk-OTUH4K4B.js.map +1 -0
- package/dist/chunk-OW73RZOU.cjs +313 -0
- package/dist/chunk-OW73RZOU.cjs.map +1 -0
- package/dist/{chunk-GFH5HOCO.js → chunk-W5O2X277.js} +42 -14
- package/dist/chunk-W5O2X277.js.map +1 -0
- package/dist/chunk-YRVE4FOD.js +57 -0
- package/dist/chunk-YRVE4FOD.js.map +1 -0
- package/dist/dashboard/DashboardCanvas.d.ts +5 -1
- package/dist/dashboard/DashboardCanvas.d.ts.map +1 -1
- package/dist/dashboard/GoldCard.d.ts +19 -0
- package/dist/dashboard/GoldCard.d.ts.map +1 -0
- package/dist/dashboard/KpiCard.d.ts.map +1 -1
- package/dist/dashboard/index.cjs +30 -26
- package/dist/dashboard/index.d.ts +1 -0
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +3 -3
- package/dist/index.cjs +198 -174
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -7
- package/dist/layout/AppShell.d.ts +27 -6
- package/dist/layout/AppShell.d.ts.map +1 -1
- package/dist/layout/ContentSplit.d.ts +32 -0
- package/dist/layout/ContentSplit.d.ts.map +1 -0
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Topbar.d.ts.map +1 -1
- package/dist/layout/index.cjs +31 -23
- package/dist/layout/index.d.ts +2 -1
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/layout/index.js +3 -3
- package/dist/primitives/index.cjs +126 -114
- package/dist/primitives/index.d.ts +2 -0
- package/dist/primitives/index.d.ts.map +1 -1
- package/dist/primitives/index.js +4 -4
- package/dist/primitives/markdown-editor.d.ts +23 -0
- package/dist/primitives/markdown-editor.d.ts.map +1 -0
- package/dist/primitives/markdown.d.ts +9 -0
- package/dist/primitives/markdown.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/dashboard/DashboardCanvas.tsx +13 -2
- package/src/dashboard/GoldCard.tsx +38 -0
- package/src/dashboard/KpiCard.tsx +4 -2
- package/src/dashboard/index.ts +1 -0
- package/src/index.ts +15 -0
- package/src/layout/AppShell.tsx +240 -20
- package/src/layout/ContentSplit.tsx +153 -0
- package/src/layout/ModulePage.tsx +2 -2
- package/src/layout/Sidebar.tsx +71 -20
- package/src/layout/Topbar.tsx +47 -17
- package/src/layout/index.ts +15 -1
- package/src/primitives/dropdown.tsx +4 -4
- package/src/primitives/index.ts +2 -0
- package/src/primitives/markdown-editor.tsx +75 -0
- package/src/primitives/markdown.tsx +169 -0
- package/src/primitives/select.tsx +1 -1
- package/dist/chunk-22J2RTMN.js.map +0 -1
- package/dist/chunk-7TBDPNLY.cjs.map +0 -1
- package/dist/chunk-CZ6X6PZT.js.map +0 -1
- package/dist/chunk-D6SXPF4F.cjs.map +0 -1
- package/dist/chunk-DNYVEOAX.js +0 -11
- package/dist/chunk-DNYVEOAX.js.map +0 -1
- package/dist/chunk-E526FHZ4.js +0 -169
- package/dist/chunk-E526FHZ4.js.map +0 -1
- package/dist/chunk-GE6XUREM.cjs.map +0 -1
- package/dist/chunk-GFH5HOCO.js.map +0 -1
- package/dist/chunk-MYZT2OG7.js.map +0 -1
- package/dist/chunk-VVVZO2LE.cjs +0 -13
- package/dist/chunk-VVVZO2LE.cjs.map +0 -1
- package/dist/chunk-W4XXXJRW.cjs +0 -196
- package/dist/chunk-W4XXXJRW.cjs.map +0 -1
- package/dist/chunk-YB5ZJKNG.cjs.map +0 -1
package/src/layout/Sidebar.tsx
CHANGED
|
@@ -7,40 +7,53 @@ import {
|
|
|
7
7
|
CreditCard,
|
|
8
8
|
Bell,
|
|
9
9
|
Calendar,
|
|
10
|
+
CalendarClock,
|
|
10
11
|
Package,
|
|
12
|
+
Activity,
|
|
11
13
|
BarChart3,
|
|
12
14
|
FileText,
|
|
13
15
|
Mail,
|
|
14
16
|
PanelLeftClose,
|
|
15
17
|
PanelLeft,
|
|
16
18
|
Search,
|
|
19
|
+
Shield,
|
|
17
20
|
DollarSign,
|
|
18
21
|
Megaphone,
|
|
22
|
+
Receipt,
|
|
19
23
|
ShoppingCart,
|
|
24
|
+
ShoppingBag,
|
|
20
25
|
Target,
|
|
21
26
|
Wrench,
|
|
22
27
|
ClipboardList,
|
|
28
|
+
ListChecks,
|
|
23
29
|
Briefcase,
|
|
24
30
|
UserCog,
|
|
25
31
|
BookOpen,
|
|
32
|
+
BookOpenCheck,
|
|
26
33
|
MessageCircle,
|
|
27
34
|
Globe,
|
|
28
35
|
Percent,
|
|
29
36
|
Tag,
|
|
37
|
+
Tags,
|
|
30
38
|
Camera,
|
|
31
39
|
UtensilsCrossed,
|
|
32
40
|
MapPin,
|
|
41
|
+
Map,
|
|
33
42
|
Handshake,
|
|
34
43
|
Contact,
|
|
35
44
|
Building2,
|
|
36
45
|
Filter,
|
|
37
46
|
Plus,
|
|
38
47
|
List,
|
|
48
|
+
ListPlus,
|
|
39
49
|
ChevronDown,
|
|
50
|
+
Ban,
|
|
51
|
+
Clock,
|
|
40
52
|
Dog,
|
|
41
53
|
Cat,
|
|
42
54
|
PawPrint,
|
|
43
55
|
Heart,
|
|
56
|
+
FolderOpen,
|
|
44
57
|
LayoutTemplate,
|
|
45
58
|
LeafyGreen,
|
|
46
59
|
Apple,
|
|
@@ -49,7 +62,12 @@ import {
|
|
|
49
62
|
PartyPopper,
|
|
50
63
|
Radio,
|
|
51
64
|
CalendarDays,
|
|
65
|
+
CalendarCheck2,
|
|
66
|
+
CalendarX,
|
|
67
|
+
BadgeDollarSign,
|
|
52
68
|
Banknote,
|
|
69
|
+
Landmark,
|
|
70
|
+
CircleDollarSign,
|
|
53
71
|
Layers,
|
|
54
72
|
Music,
|
|
55
73
|
Eye,
|
|
@@ -61,8 +79,16 @@ import {
|
|
|
61
79
|
Award,
|
|
62
80
|
LayoutDashboard,
|
|
63
81
|
MessageSquare,
|
|
82
|
+
Inbox,
|
|
64
83
|
Star,
|
|
84
|
+
TrendingUp,
|
|
85
|
+
UserCheck,
|
|
86
|
+
UserPlus,
|
|
87
|
+
UserX,
|
|
65
88
|
Workflow,
|
|
89
|
+
Boxes,
|
|
90
|
+
Clock3,
|
|
91
|
+
FileCheck2,
|
|
66
92
|
type LucideIcon,
|
|
67
93
|
} from 'lucide-react'
|
|
68
94
|
import { cn } from '../utils/cn'
|
|
@@ -112,13 +138,13 @@ export interface SidebarProps {
|
|
|
112
138
|
}
|
|
113
139
|
|
|
114
140
|
const ICON_MAP: Record<string, LucideIcon> = {
|
|
115
|
-
Home, Users, Settings, CreditCard, Bell, Calendar, Package, BarChart3,
|
|
141
|
+
Home, Users, Settings, CreditCard, Bell, Calendar, CalendarClock, Package, Activity, BarChart3,
|
|
116
142
|
FileText, Mail, DollarSign, Megaphone, ShoppingCart, Target, Wrench,
|
|
117
|
-
ClipboardList, Briefcase, UserCog, BookOpen, MessageCircle, Globe,
|
|
118
|
-
Percent, Tag, Camera, UtensilsCrossed, MapPin, Handshake, Contact,
|
|
119
|
-
Building2, Filter, Plus, List, Search, Dog, Cat, PawPrint, Heart, LayoutTemplate, LeafyGreen, Apple, Egg, Wheat,
|
|
120
|
-
PartyPopper, Radio, CalendarDays, Banknote, Layers, Music, Eye, ListMusic, Disc3, UsersRound, Mic,
|
|
121
|
-
Zap, Award, LayoutDashboard, MessageSquare, Star, Workflow,
|
|
143
|
+
Receipt, ShoppingBag, ClipboardList, ListChecks, Briefcase, UserCog, BookOpen, BookOpenCheck, MessageCircle, Globe,
|
|
144
|
+
Percent, Tag, Tags, Camera, UtensilsCrossed, MapPin, Map, Handshake, Contact,
|
|
145
|
+
Building2, Filter, Plus, List, ListPlus, Search, Shield, Ban, Clock, Dog, Cat, PawPrint, Heart, FolderOpen, LayoutTemplate, LeafyGreen, Apple, Egg, Wheat,
|
|
146
|
+
PartyPopper, Radio, CalendarDays, CalendarCheck2, CalendarX, BadgeDollarSign, Banknote, Landmark, CircleDollarSign, Layers, Music, Eye, ListMusic, Disc3, UsersRound, Mic,
|
|
147
|
+
Zap, Award, LayoutDashboard, MessageSquare, Inbox, Star, TrendingUp, UserCheck, UserPlus, UserX, Workflow, Boxes, Clock3, FileCheck2,
|
|
122
148
|
}
|
|
123
149
|
|
|
124
150
|
export { ICON_MAP }
|
|
@@ -192,24 +218,45 @@ function NavItem({
|
|
|
192
218
|
return content
|
|
193
219
|
}
|
|
194
220
|
|
|
221
|
+
/** How specifically `route` matches `path` (-1 = no match; higher = more specific).
|
|
222
|
+
* An exact match beats a prefix match; a longer prefix beats a shorter one. */
|
|
223
|
+
function routeMatchScore(route: string, path: string): number {
|
|
224
|
+
if (path === route) return route.length + 1
|
|
225
|
+
if (route !== '/' && path.startsWith(route + '/')) return route.length
|
|
226
|
+
return -1
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The single most-specific nav route that matches the current path (longest wins).
|
|
230
|
+
* Prevents a parent like `/courses` from staying active on `/courses/sales`. */
|
|
231
|
+
function resolveActiveRoute(navigation: NavigationItem[], path: string): string | null {
|
|
232
|
+
let best: string | null = null
|
|
233
|
+
let bestScore = 0
|
|
234
|
+
const consider = (route?: string) => {
|
|
235
|
+
if (!route) return
|
|
236
|
+
const score = routeMatchScore(route, path)
|
|
237
|
+
if (score > bestScore) { bestScore = score; best = route }
|
|
238
|
+
}
|
|
239
|
+
for (const item of navigation) {
|
|
240
|
+
consider(item.route)
|
|
241
|
+
for (const child of item.children ?? []) consider(child.route)
|
|
242
|
+
}
|
|
243
|
+
return best
|
|
244
|
+
}
|
|
245
|
+
|
|
195
246
|
function NavGroup({
|
|
196
247
|
item,
|
|
197
248
|
collapsed,
|
|
198
|
-
|
|
249
|
+
activeRoute,
|
|
199
250
|
onNavigate,
|
|
200
251
|
}: {
|
|
201
252
|
item: NavigationItem
|
|
202
253
|
collapsed: boolean
|
|
203
|
-
|
|
254
|
+
activeRoute: string | null
|
|
204
255
|
onNavigate: (route: string) => void
|
|
205
256
|
}) {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
)
|
|
257
|
+
const isChildActive = item.children?.some((c) => c.route === activeRoute)
|
|
258
|
+
const [open, setOpen] = React.useState(() => isChildActive ?? false)
|
|
209
259
|
const Icon = getIcon(item.icon)
|
|
210
|
-
const isChildActive = item.children?.some(
|
|
211
|
-
(c) => currentPath === c.route || currentPath.startsWith(c.route + '/')
|
|
212
|
-
)
|
|
213
260
|
|
|
214
261
|
if (collapsed) {
|
|
215
262
|
return (
|
|
@@ -254,7 +301,7 @@ function NavGroup({
|
|
|
254
301
|
<div className="ml-4 mt-0.5 space-y-0.5 border-l border-sidebar-border/50 pl-3">
|
|
255
302
|
{item.children?.map((child) => {
|
|
256
303
|
const ChildIcon = getIcon(child.icon)
|
|
257
|
-
const isActive =
|
|
304
|
+
const isActive = child.route === activeRoute
|
|
258
305
|
return (
|
|
259
306
|
<button
|
|
260
307
|
key={child.id}
|
|
@@ -296,6 +343,10 @@ export function Sidebar({
|
|
|
296
343
|
const currentPath = currentPathProp
|
|
297
344
|
const setCommandPaletteOpen = useLayoutStore((s) => s.setCommandPaletteOpen)
|
|
298
345
|
|
|
346
|
+
// Most-specific match wins, so sibling routes that share a prefix (e.g.
|
|
347
|
+
// /courses vs /courses/sales) don't both light up.
|
|
348
|
+
const activeRoute = resolveActiveRoute(navigation, currentPath)
|
|
349
|
+
|
|
299
350
|
const mainItems = navigation.filter((item) => item.section === 'main')
|
|
300
351
|
const secondaryItems = navigation.filter((item) => item.section === 'secondary')
|
|
301
352
|
const settingsItems = navigation.filter((item) => item.section === 'settings')
|
|
@@ -353,13 +404,13 @@ export function Sidebar({
|
|
|
353
404
|
<div className="space-y-1">
|
|
354
405
|
{mainItems.map((item) =>
|
|
355
406
|
item.children && item.children.length > 0 ? (
|
|
356
|
-
<NavGroup key={item.id} item={item} collapsed={collapsed}
|
|
407
|
+
<NavGroup key={item.id} item={item} collapsed={collapsed} activeRoute={activeRoute} onNavigate={handleNavigate} />
|
|
357
408
|
) : (
|
|
358
409
|
<NavItem
|
|
359
410
|
key={item.id}
|
|
360
411
|
item={item}
|
|
361
412
|
collapsed={collapsed}
|
|
362
|
-
isActive={
|
|
413
|
+
isActive={item.route === activeRoute}
|
|
363
414
|
onNavigate={handleNavigate}
|
|
364
415
|
/>
|
|
365
416
|
)
|
|
@@ -371,13 +422,13 @@ export function Sidebar({
|
|
|
371
422
|
<div className="mt-6 space-y-1">
|
|
372
423
|
{secondaryItems.map((item) =>
|
|
373
424
|
item.children && item.children.length > 0 ? (
|
|
374
|
-
<NavGroup key={item.id} item={item} collapsed={collapsed}
|
|
425
|
+
<NavGroup key={item.id} item={item} collapsed={collapsed} activeRoute={activeRoute} onNavigate={handleNavigate} />
|
|
375
426
|
) : (
|
|
376
427
|
<NavItem
|
|
377
428
|
key={item.id}
|
|
378
429
|
item={item}
|
|
379
430
|
collapsed={collapsed}
|
|
380
|
-
isActive={
|
|
431
|
+
isActive={item.route === activeRoute}
|
|
381
432
|
onNavigate={handleNavigate}
|
|
382
433
|
/>
|
|
383
434
|
)
|
|
@@ -394,7 +445,7 @@ export function Sidebar({
|
|
|
394
445
|
key={item.id}
|
|
395
446
|
item={item}
|
|
396
447
|
collapsed={collapsed}
|
|
397
|
-
isActive={
|
|
448
|
+
isActive={item.route === activeRoute}
|
|
398
449
|
onNavigate={handleNavigate}
|
|
399
450
|
/>
|
|
400
451
|
))}
|
package/src/layout/Topbar.tsx
CHANGED
|
@@ -11,23 +11,30 @@ import {
|
|
|
11
11
|
CreditCard,
|
|
12
12
|
Bell,
|
|
13
13
|
Calendar,
|
|
14
|
+
CalendarClock,
|
|
14
15
|
Package,
|
|
16
|
+
Activity,
|
|
15
17
|
BarChart3,
|
|
18
|
+
Clapperboard,
|
|
16
19
|
FileText,
|
|
17
20
|
Mail,
|
|
18
21
|
DollarSign,
|
|
19
22
|
Megaphone,
|
|
20
23
|
ShoppingCart,
|
|
24
|
+
ShoppingBag,
|
|
21
25
|
Target,
|
|
22
26
|
Wrench,
|
|
23
27
|
ClipboardList,
|
|
28
|
+
ClipboardCheck,
|
|
24
29
|
Briefcase,
|
|
25
30
|
UserCog,
|
|
26
31
|
BookOpen,
|
|
32
|
+
BookOpenCheck,
|
|
27
33
|
MessageCircle,
|
|
28
34
|
Globe,
|
|
29
35
|
Percent,
|
|
30
36
|
Tag,
|
|
37
|
+
Tags,
|
|
31
38
|
Camera,
|
|
32
39
|
UtensilsCrossed,
|
|
33
40
|
MapPin,
|
|
@@ -35,7 +42,11 @@ import {
|
|
|
35
42
|
Contact,
|
|
36
43
|
Building2,
|
|
37
44
|
Plus,
|
|
45
|
+
Shield,
|
|
38
46
|
List,
|
|
47
|
+
ListChecks,
|
|
48
|
+
FileCheck2,
|
|
49
|
+
FolderOpen,
|
|
39
50
|
Dog,
|
|
40
51
|
Cat,
|
|
41
52
|
PawPrint,
|
|
@@ -64,18 +75,32 @@ import {
|
|
|
64
75
|
SlidersHorizontal,
|
|
65
76
|
TableProperties,
|
|
66
77
|
UserPlus,
|
|
78
|
+
UserCheck,
|
|
79
|
+
UserX,
|
|
67
80
|
TreePalm,
|
|
81
|
+
Boxes,
|
|
82
|
+
Box,
|
|
68
83
|
PartyPopper,
|
|
69
84
|
Radio,
|
|
70
85
|
CalendarDays,
|
|
86
|
+
CalendarCheck2,
|
|
87
|
+
CalendarX,
|
|
88
|
+
BadgeDollarSign,
|
|
71
89
|
Banknote,
|
|
72
90
|
Layers,
|
|
73
91
|
Music,
|
|
74
92
|
Eye,
|
|
75
93
|
ListMusic,
|
|
94
|
+
ListPlus,
|
|
95
|
+
Map,
|
|
96
|
+
Ban,
|
|
97
|
+
Clock3,
|
|
76
98
|
Disc3,
|
|
77
99
|
UsersRound,
|
|
78
100
|
Mic,
|
|
101
|
+
Inbox,
|
|
102
|
+
Star,
|
|
103
|
+
Zap,
|
|
79
104
|
type LucideIcon,
|
|
80
105
|
} from 'lucide-react'
|
|
81
106
|
import { cn } from '../utils/cn'
|
|
@@ -175,13 +200,14 @@ function TopbarUserMenu({
|
|
|
175
200
|
}
|
|
176
201
|
|
|
177
202
|
export const ICON_MAP: Record<string, LucideIcon> = {
|
|
178
|
-
Home, Users, Settings, CreditCard, Bell, Calendar, Package, BarChart3,
|
|
179
|
-
FileText, Mail, DollarSign, Megaphone, ShoppingCart, Target, Wrench,
|
|
180
|
-
ClipboardList, Briefcase, UserCog, BookOpen, MessageCircle, Globe,
|
|
181
|
-
Percent, Tag, Camera, UtensilsCrossed, Search, MapPin, Handshake,
|
|
182
|
-
Contact, Building2, ChevronDown, Filter, Plus, List, Dog, Cat, PawPrint, Heart, LayoutTemplate, LeafyGreen, Apple, Egg, Wheat,
|
|
183
|
-
ArrowDownCircle, ArrowUpCircle, ArrowLeftRight, Plug, SlidersHorizontal, TableProperties, Landmark, Receipt, TrendingUp, CircleDollarSign, Clock, AlertTriangle, Sparkles, Wallet, Warehouse, Ruler, ArrowUpRight, ArrowDownRight, UserPlus, TreePalm,
|
|
184
|
-
PartyPopper, Radio, CalendarDays, Banknote, Layers, Music, Eye, ListMusic, Disc3, UsersRound, Mic,
|
|
203
|
+
Home, Users, Settings, CreditCard, Bell, Calendar, CalendarClock, Package, Activity, BarChart3,
|
|
204
|
+
FileText, Mail, DollarSign, Megaphone, ShoppingCart, ShoppingBag, Target, Wrench,
|
|
205
|
+
ClipboardList, ClipboardCheck, Briefcase, UserCog, BookOpen, MessageCircle, Globe,
|
|
206
|
+
BookOpenCheck, Percent, Tag, Tags, Camera, UtensilsCrossed, Search, MapPin, Handshake,
|
|
207
|
+
Contact, Building2, ChevronDown, Filter, Plus, Shield, List, ListChecks, FolderOpen, Dog, Cat, PawPrint, Heart, LayoutTemplate, LeafyGreen, Apple, Egg, Wheat,
|
|
208
|
+
ArrowDownCircle, ArrowUpCircle, ArrowLeftRight, Plug, SlidersHorizontal, TableProperties, Landmark, Receipt, TrendingUp, CircleDollarSign, Clock, AlertTriangle, Sparkles, Wallet, Warehouse, Ruler, ArrowUpRight, ArrowDownRight, UserPlus, UserX, TreePalm,
|
|
209
|
+
Boxes, Box, FileCheck2, PartyPopper, Radio, CalendarDays, BadgeDollarSign, Banknote, Layers, Music, Eye, ListMusic, Disc3, UsersRound, Mic,
|
|
210
|
+
CalendarCheck2, CalendarX, ListPlus, Map, Ban, Clock3, Inbox, UserCheck, Star, Zap, Clapperboard,
|
|
185
211
|
}
|
|
186
212
|
|
|
187
213
|
function getIcon(name: string): LucideIcon {
|
|
@@ -281,15 +307,19 @@ export function Topbar({
|
|
|
281
307
|
{/* Row 1: Logo + Search + Actions */}
|
|
282
308
|
<div className="border-b border-sidebar-border bg-sidebar text-sidebar-foreground">
|
|
283
309
|
<div className="flex h-14 w-full items-center justify-between px-4 md:px-6">
|
|
284
|
-
{/* Left: Mobile menu + Logo
|
|
310
|
+
{/* Left: Mobile menu + Logo — the hamburger is only rendered when an
|
|
311
|
+
onMenuClick handler is supplied. Mobile-first apps with a bottom
|
|
312
|
+
nav omit it so the bottom bar is the primary navigation. */}
|
|
285
313
|
<div className="flex items-center gap-3">
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
314
|
+
{onMenuClick && (
|
|
315
|
+
<button
|
|
316
|
+
onClick={onMenuClick}
|
|
317
|
+
className="inline-flex items-center justify-center rounded-md p-2 text-sidebar-foreground/70 hover:bg-sidebar/30 hover:text-sidebar-foreground md:hidden"
|
|
318
|
+
aria-label="Open menu"
|
|
319
|
+
>
|
|
320
|
+
<Menu className="h-5 w-5" />
|
|
321
|
+
</button>
|
|
322
|
+
)}
|
|
293
323
|
<div className="hidden shrink-0 items-center md:flex">
|
|
294
324
|
{logo ?? <span className="text-lg font-bold">App</span>}
|
|
295
325
|
</div>
|
|
@@ -299,7 +329,7 @@ export function Topbar({
|
|
|
299
329
|
<div className="mx-8 hidden max-w-sm flex-1 md:flex">
|
|
300
330
|
<button
|
|
301
331
|
onClick={() => setCommandPaletteOpen(true)}
|
|
302
|
-
className="relative flex h-8 w-full items-center rounded-md border border-sidebar-border bg-sidebar-accent px-3 pl-8 text-sm text-sidebar-muted shadow-sm transition-colors hover:bg-
|
|
332
|
+
className="relative flex h-8 w-full items-center rounded-md border border-sidebar-border bg-sidebar-accent px-3 pl-8 text-sm text-sidebar-muted shadow-sm transition-colors hover:border-sidebar-foreground/25 hover:bg-sidebar-foreground/10 hover:text-sidebar-foreground"
|
|
303
333
|
>
|
|
304
334
|
<Search className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-sidebar-muted" />
|
|
305
335
|
<span className="text-xs">{searchPlaceholder}</span>
|
|
@@ -335,7 +365,7 @@ export function Topbar({
|
|
|
335
365
|
const left = active.offsetLeft - el.offsetWidth / 2 + active.offsetWidth / 2
|
|
336
366
|
el.scrollTo({ left: Math.max(0, left), behavior: 'smooth' })
|
|
337
367
|
}
|
|
338
|
-
}} className="flex items-center gap-0.5 overflow-x-auto scrollbar-
|
|
368
|
+
}} className="flex items-center gap-0.5 overflow-x-auto scrollbar-none">
|
|
339
369
|
{allNav.map((item) => {
|
|
340
370
|
if (item.children && item.children.length > 0) {
|
|
341
371
|
return (
|
package/src/layout/index.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
AppShell,
|
|
3
|
+
type AppShellProps,
|
|
4
|
+
type AppShellUser,
|
|
5
|
+
type BottomNavItem,
|
|
6
|
+
type BottomNavRouteItem,
|
|
7
|
+
type BottomNavActionItem,
|
|
8
|
+
type MobileHeaderVariant,
|
|
9
|
+
} from './AppShell'
|
|
2
10
|
export { Sidebar, type SidebarProps, type SidebarUser, type NavigationItem, ICON_MAP } from './Sidebar'
|
|
3
11
|
export { Topbar, type TopbarProps } from './Topbar'
|
|
4
12
|
export {
|
|
@@ -23,3 +31,9 @@ export {
|
|
|
23
31
|
useNavTransition,
|
|
24
32
|
type NavTransition,
|
|
25
33
|
} from './PageTransition'
|
|
34
|
+
export {
|
|
35
|
+
ContentSplit,
|
|
36
|
+
ContentSplitTrigger,
|
|
37
|
+
type ContentSplitProps,
|
|
38
|
+
type ContentSplitTriggerProps,
|
|
39
|
+
} from './ContentSplit'
|
|
@@ -25,7 +25,7 @@ const DropdownSubTrigger = React.forwardRef<
|
|
|
25
25
|
<DropdownMenuPrimitive.SubTrigger
|
|
26
26
|
ref={ref}
|
|
27
27
|
className={cn(
|
|
28
|
-
'flex cursor-default select-none items-center rounded-
|
|
28
|
+
'flex cursor-default select-none items-center rounded-button px-2 py-1.5 text-sm outline-none focus:bg-muted data-[state=open]:bg-muted',
|
|
29
29
|
inset && 'pl-8',
|
|
30
30
|
className
|
|
31
31
|
)}
|
|
@@ -79,7 +79,7 @@ const DropdownItem = React.forwardRef<
|
|
|
79
79
|
<DropdownMenuPrimitive.Item
|
|
80
80
|
ref={ref}
|
|
81
81
|
className={cn(
|
|
82
|
-
'relative flex cursor-default select-none items-center rounded-
|
|
82
|
+
'relative flex cursor-default select-none items-center rounded-button px-2 py-1.5 text-sm outline-none transition-colors focus:bg-muted data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
83
83
|
inset && 'pl-8',
|
|
84
84
|
className
|
|
85
85
|
)}
|
|
@@ -95,7 +95,7 @@ const DropdownCheckboxItem = React.forwardRef<
|
|
|
95
95
|
<DropdownMenuPrimitive.CheckboxItem
|
|
96
96
|
ref={ref}
|
|
97
97
|
className={cn(
|
|
98
|
-
'relative flex cursor-default select-none items-center rounded-
|
|
98
|
+
'relative flex cursor-default select-none items-center rounded-button py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-muted data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
99
99
|
className
|
|
100
100
|
)}
|
|
101
101
|
checked={checked}
|
|
@@ -119,7 +119,7 @@ const DropdownRadioItem = React.forwardRef<
|
|
|
119
119
|
<DropdownMenuPrimitive.RadioItem
|
|
120
120
|
ref={ref}
|
|
121
121
|
className={cn(
|
|
122
|
-
'relative flex cursor-default select-none items-center rounded-
|
|
122
|
+
'relative flex cursor-default select-none items-center rounded-button py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-muted data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
123
123
|
className
|
|
124
124
|
)}
|
|
125
125
|
{...props}
|
package/src/primitives/index.ts
CHANGED
|
@@ -81,3 +81,5 @@ export { CurrencyInput } from './currency-input'
|
|
|
81
81
|
export { ConfirmDialog } from './confirm-dialog'
|
|
82
82
|
export { Popover, PopoverTrigger, PopoverContent } from './popover'
|
|
83
83
|
export { SegmentedControl, type SegmentedControlProps, type SegmentedOption } from './segmented-control'
|
|
84
|
+
export { Markdown, renderMarkdown, type MarkdownProps } from './markdown'
|
|
85
|
+
export { MarkdownEditor, type MarkdownEditorProps } from './markdown-editor'
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cn } from '../utils/cn'
|
|
3
|
+
import { SegmentedControl } from './segmented-control'
|
|
4
|
+
import { Markdown } from './markdown'
|
|
5
|
+
|
|
6
|
+
export interface MarkdownEditorProps {
|
|
7
|
+
value: string
|
|
8
|
+
onChange: (value: string) => void
|
|
9
|
+
placeholder?: string
|
|
10
|
+
/** Minimum visible rows for the textarea. */
|
|
11
|
+
minRows?: number
|
|
12
|
+
defaultMode?: 'edit' | 'preview'
|
|
13
|
+
/** i18n injection — ui stays locale-agnostic. */
|
|
14
|
+
editLabel?: string
|
|
15
|
+
previewLabel?: string
|
|
16
|
+
/** Custom preview renderer — e.g. a social-post mockup instead of plain
|
|
17
|
+
* markdown. Defaults to the built-in Markdown renderer. */
|
|
18
|
+
renderPreview?: (value: string) => React.ReactNode
|
|
19
|
+
className?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Markdown editor with an edit/preview toggle. Plain textarea + the zero-dep
|
|
24
|
+
* Markdown renderer — deliberately not a block/WYSIWYG editor (that's a later
|
|
25
|
+
* milestone); this covers script/brief documents today.
|
|
26
|
+
*/
|
|
27
|
+
export function MarkdownEditor({
|
|
28
|
+
value,
|
|
29
|
+
onChange,
|
|
30
|
+
placeholder,
|
|
31
|
+
minRows = 16,
|
|
32
|
+
defaultMode = 'preview',
|
|
33
|
+
editLabel = 'Edit',
|
|
34
|
+
previewLabel = 'Preview',
|
|
35
|
+
renderPreview,
|
|
36
|
+
className,
|
|
37
|
+
}: MarkdownEditorProps) {
|
|
38
|
+
const [mode, setMode] = React.useState<'edit' | 'preview'>(
|
|
39
|
+
defaultMode === 'preview' && value.trim() === '' ? 'edit' : defaultMode,
|
|
40
|
+
)
|
|
41
|
+
return (
|
|
42
|
+
<div className={cn('flex flex-col gap-2', className)}>
|
|
43
|
+
<div className="flex justify-end">
|
|
44
|
+
<SegmentedControl
|
|
45
|
+
aria-label="Markdown mode"
|
|
46
|
+
options={[
|
|
47
|
+
{ value: 'edit', label: editLabel },
|
|
48
|
+
{ value: 'preview', label: previewLabel },
|
|
49
|
+
]}
|
|
50
|
+
value={mode}
|
|
51
|
+
onChange={(m) => setMode(m as 'edit' | 'preview')}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
{mode === 'edit' ? (
|
|
55
|
+
<textarea
|
|
56
|
+
value={value}
|
|
57
|
+
onChange={(e) => onChange(e.target.value)}
|
|
58
|
+
placeholder={placeholder}
|
|
59
|
+
rows={Math.max(minRows, value.split('\n').length + 1)}
|
|
60
|
+
className={cn(
|
|
61
|
+
'w-full resize-y rounded-md border border-border bg-card p-3',
|
|
62
|
+
'font-mono text-[13px] leading-relaxed text-foreground',
|
|
63
|
+
'placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring',
|
|
64
|
+
)}
|
|
65
|
+
/>
|
|
66
|
+
) : renderPreview ? (
|
|
67
|
+
renderPreview(value)
|
|
68
|
+
) : (
|
|
69
|
+
<div className="rounded-md border border-border bg-card p-4">
|
|
70
|
+
<Markdown source={value} />
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
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
|
+
}
|
|
@@ -116,7 +116,7 @@ const SelectItem = React.forwardRef<
|
|
|
116
116
|
<SelectPrimitive.Item
|
|
117
117
|
ref={ref}
|
|
118
118
|
className={cn(
|
|
119
|
-
'relative flex w-full cursor-default select-none items-center rounded-
|
|
119
|
+
'relative flex w-full cursor-default select-none items-center rounded-button py-1.5 pl-8 pr-2 text-sm outline-none data-[highlighted]:bg-muted focus:bg-muted data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
120
120
|
className
|
|
121
121
|
)}
|
|
122
122
|
{...props}
|