@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.
Files changed (88) hide show
  1. package/dist/chunk-75EFDLST.js +274 -0
  2. package/dist/chunk-75EFDLST.js.map +1 -0
  3. package/dist/{chunk-YB5ZJKNG.cjs → chunk-7PTRAWHO.cjs} +263 -225
  4. package/dist/chunk-7PTRAWHO.cjs.map +1 -0
  5. package/dist/{chunk-CZ6X6PZT.js → chunk-COC5DDFG.js} +313 -44
  6. package/dist/chunk-COC5DDFG.js.map +1 -0
  7. package/dist/{chunk-MYZT2OG7.js → chunk-FY7OZNKU.js} +24 -68
  8. package/dist/chunk-FY7OZNKU.js.map +1 -0
  9. package/dist/chunk-H6OULCHF.cjs +81 -0
  10. package/dist/chunk-H6OULCHF.cjs.map +1 -0
  11. package/dist/{chunk-GE6XUREM.cjs → chunk-HG5RH3B3.cjs} +339 -68
  12. package/dist/chunk-HG5RH3B3.cjs.map +1 -0
  13. package/dist/{chunk-7TBDPNLY.cjs → chunk-NWPMNPGR.cjs} +76 -47
  14. package/dist/chunk-NWPMNPGR.cjs.map +1 -0
  15. package/dist/{chunk-D6SXPF4F.cjs → chunk-ODY3GZ3Y.cjs} +41 -87
  16. package/dist/chunk-ODY3GZ3Y.cjs.map +1 -0
  17. package/dist/{chunk-22J2RTMN.js → chunk-OTUH4K4B.js} +196 -150
  18. package/dist/chunk-OTUH4K4B.js.map +1 -0
  19. package/dist/chunk-OW73RZOU.cjs +313 -0
  20. package/dist/chunk-OW73RZOU.cjs.map +1 -0
  21. package/dist/{chunk-GFH5HOCO.js → chunk-W5O2X277.js} +42 -14
  22. package/dist/chunk-W5O2X277.js.map +1 -0
  23. package/dist/chunk-YRVE4FOD.js +57 -0
  24. package/dist/chunk-YRVE4FOD.js.map +1 -0
  25. package/dist/dashboard/DashboardCanvas.d.ts +5 -1
  26. package/dist/dashboard/DashboardCanvas.d.ts.map +1 -1
  27. package/dist/dashboard/GoldCard.d.ts +19 -0
  28. package/dist/dashboard/GoldCard.d.ts.map +1 -0
  29. package/dist/dashboard/KpiCard.d.ts.map +1 -1
  30. package/dist/dashboard/index.cjs +30 -26
  31. package/dist/dashboard/index.d.ts +1 -0
  32. package/dist/dashboard/index.d.ts.map +1 -1
  33. package/dist/dashboard/index.js +3 -3
  34. package/dist/index.cjs +198 -174
  35. package/dist/index.d.ts +3 -3
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +7 -7
  38. package/dist/layout/AppShell.d.ts +27 -6
  39. package/dist/layout/AppShell.d.ts.map +1 -1
  40. package/dist/layout/ContentSplit.d.ts +32 -0
  41. package/dist/layout/ContentSplit.d.ts.map +1 -0
  42. package/dist/layout/Sidebar.d.ts.map +1 -1
  43. package/dist/layout/Topbar.d.ts.map +1 -1
  44. package/dist/layout/index.cjs +31 -23
  45. package/dist/layout/index.d.ts +2 -1
  46. package/dist/layout/index.d.ts.map +1 -1
  47. package/dist/layout/index.js +3 -3
  48. package/dist/primitives/index.cjs +126 -114
  49. package/dist/primitives/index.d.ts +2 -0
  50. package/dist/primitives/index.d.ts.map +1 -1
  51. package/dist/primitives/index.js +4 -4
  52. package/dist/primitives/markdown-editor.d.ts +23 -0
  53. package/dist/primitives/markdown-editor.d.ts.map +1 -0
  54. package/dist/primitives/markdown.d.ts +9 -0
  55. package/dist/primitives/markdown.d.ts.map +1 -0
  56. package/package.json +3 -3
  57. package/src/dashboard/DashboardCanvas.tsx +13 -2
  58. package/src/dashboard/GoldCard.tsx +38 -0
  59. package/src/dashboard/KpiCard.tsx +4 -2
  60. package/src/dashboard/index.ts +1 -0
  61. package/src/index.ts +15 -0
  62. package/src/layout/AppShell.tsx +240 -20
  63. package/src/layout/ContentSplit.tsx +153 -0
  64. package/src/layout/ModulePage.tsx +2 -2
  65. package/src/layout/Sidebar.tsx +71 -20
  66. package/src/layout/Topbar.tsx +47 -17
  67. package/src/layout/index.ts +15 -1
  68. package/src/primitives/dropdown.tsx +4 -4
  69. package/src/primitives/index.ts +2 -0
  70. package/src/primitives/markdown-editor.tsx +75 -0
  71. package/src/primitives/markdown.tsx +169 -0
  72. package/src/primitives/select.tsx +1 -1
  73. package/dist/chunk-22J2RTMN.js.map +0 -1
  74. package/dist/chunk-7TBDPNLY.cjs.map +0 -1
  75. package/dist/chunk-CZ6X6PZT.js.map +0 -1
  76. package/dist/chunk-D6SXPF4F.cjs.map +0 -1
  77. package/dist/chunk-DNYVEOAX.js +0 -11
  78. package/dist/chunk-DNYVEOAX.js.map +0 -1
  79. package/dist/chunk-E526FHZ4.js +0 -169
  80. package/dist/chunk-E526FHZ4.js.map +0 -1
  81. package/dist/chunk-GE6XUREM.cjs.map +0 -1
  82. package/dist/chunk-GFH5HOCO.js.map +0 -1
  83. package/dist/chunk-MYZT2OG7.js.map +0 -1
  84. package/dist/chunk-VVVZO2LE.cjs +0 -13
  85. package/dist/chunk-VVVZO2LE.cjs.map +0 -1
  86. package/dist/chunk-W4XXXJRW.cjs +0 -196
  87. package/dist/chunk-W4XXXJRW.cjs.map +0 -1
  88. package/dist/chunk-YB5ZJKNG.cjs.map +0 -1
@@ -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
- currentPath,
249
+ activeRoute,
199
250
  onNavigate,
200
251
  }: {
201
252
  item: NavigationItem
202
253
  collapsed: boolean
203
- currentPath: string
254
+ activeRoute: string | null
204
255
  onNavigate: (route: string) => void
205
256
  }) {
206
- const [open, setOpen] = React.useState(() =>
207
- item.children?.some((c) => currentPath === c.route || currentPath.startsWith(c.route + '/')) ?? false
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 = currentPath === child.route || currentPath.startsWith(child.route + '/')
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} currentPath={currentPath} onNavigate={handleNavigate} />
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={currentPath === item.route || currentPath.startsWith(item.route + '/')}
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} currentPath={currentPath} onNavigate={handleNavigate} />
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={currentPath === item.route || currentPath.startsWith(item.route + '/')}
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={currentPath === item.route || currentPath.startsWith(item.route + '/')}
448
+ isActive={item.route === activeRoute}
398
449
  onNavigate={handleNavigate}
399
450
  />
400
451
  ))}
@@ -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
- <button
287
- onClick={onMenuClick}
288
- 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"
289
- aria-label="Open menu"
290
- >
291
- <Menu className="h-5 w-5" />
292
- </button>
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-card"
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-hide">
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 (
@@ -1,4 +1,12 @@
1
- export { AppShell, type AppShellProps, type AppShellUser } from './AppShell'
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-sm px-2 py-1.5 text-sm outline-none focus:bg-muted data-[state=open]:bg-muted',
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-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-muted data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
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-sm 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',
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-sm 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',
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}
@@ -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-sm 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',
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}