@fayz-ai/ui 0.8.3 → 0.8.5

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/ChartWidgetInner-GDE5PBTR.js +46 -0
  2. package/dist/ChartWidgetInner-GDE5PBTR.js.map +1 -0
  3. package/dist/{chunk-BVF2KPTT.js → chunk-G6OKZ7WL.js} +30 -3
  4. package/dist/chunk-G6OKZ7WL.js.map +1 -0
  5. package/dist/{chunk-ID2HQK2Q.js → chunk-KV3MHB6N.js} +116 -213
  6. package/dist/chunk-KV3MHB6N.js.map +1 -0
  7. package/dist/{chunk-A7O7KGNL.js → chunk-OUPJDGKE.js} +27 -43
  8. package/dist/chunk-OUPJDGKE.js.map +1 -0
  9. package/dist/chunk-PVIB724L.js +161 -0
  10. package/dist/chunk-PVIB724L.js.map +1 -0
  11. package/dist/{chunk-O5F4LLSL.js → chunk-VDAYIQX4.js} +208 -8
  12. package/dist/chunk-VDAYIQX4.js.map +1 -0
  13. package/dist/{chunk-2GDQUARM.js → chunk-YLQGUJAQ.js} +24 -42
  14. package/dist/chunk-YLQGUJAQ.js.map +1 -0
  15. package/dist/dashboard/ChartWidget.d.ts.map +1 -1
  16. package/dist/dashboard/ChartWidgetInner.d.ts +11 -0
  17. package/dist/dashboard/ChartWidgetInner.d.ts.map +1 -0
  18. package/dist/dashboard/OnboardingWidget.d.ts.map +1 -1
  19. package/dist/dashboard/index.js +3 -2
  20. package/dist/icons.d.ts +8 -0
  21. package/dist/icons.d.ts.map +1 -0
  22. package/dist/icons.js +3 -0
  23. package/dist/icons.js.map +1 -0
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +6 -6
  27. package/dist/layout/Sidebar.d.ts +5 -2
  28. package/dist/layout/Sidebar.d.ts.map +1 -1
  29. package/dist/layout/index.js +2 -1
  30. package/dist/preset.cjs +201 -11
  31. package/dist/preset.cjs.map +1 -1
  32. package/dist/preset.js +201 -1
  33. package/dist/preset.js.map +1 -1
  34. package/dist/primitives/date-picker.d.ts +7 -1
  35. package/dist/primitives/date-picker.d.ts.map +1 -1
  36. package/dist/primitives/index.js +2 -2
  37. package/dist/primitives/search-select.d.ts +3 -1
  38. package/dist/primitives/search-select.d.ts.map +1 -1
  39. package/dist/tailwind.cjs +199 -2
  40. package/dist/tailwind.cjs.map +1 -1
  41. package/dist/tailwind.js +198 -1
  42. package/dist/tailwind.js.map +1 -1
  43. package/dist/theme/index.d.ts.map +1 -1
  44. package/dist/theme/index.js +1 -2
  45. package/package.json +24 -22
  46. package/src/dashboard/ChartWidget.tsx +8 -65
  47. package/src/dashboard/ChartWidgetInner.tsx +83 -0
  48. package/src/dashboard/OnboardingWidget.tsx +32 -1
  49. package/src/dashboard/icon.ts +2 -2
  50. package/src/icons.ts +66 -0
  51. package/src/index.ts +4 -0
  52. package/src/layout/Sidebar.tsx +70 -136
  53. package/src/primitives/date-picker.tsx +30 -4
  54. package/src/primitives/search-select.tsx +12 -4
  55. package/src/theme/index.ts +9 -6
  56. package/dist/chunk-2GDQUARM.js.map +0 -1
  57. package/dist/chunk-2P7T6QQK.cjs +0 -206
  58. package/dist/chunk-2P7T6QQK.cjs.map +0 -1
  59. package/dist/chunk-A7O7KGNL.js.map +0 -1
  60. package/dist/chunk-BVF2KPTT.js.map +0 -1
  61. package/dist/chunk-H6OULCHF.cjs +0 -81
  62. package/dist/chunk-H6OULCHF.cjs.map +0 -1
  63. package/dist/chunk-ID2HQK2Q.js.map +0 -1
  64. package/dist/chunk-IWBXYV4U.js +0 -203
  65. package/dist/chunk-IWBXYV4U.js.map +0 -1
  66. package/dist/chunk-MI6FOB4O.cjs +0 -351
  67. package/dist/chunk-MI6FOB4O.cjs.map +0 -1
  68. package/dist/chunk-O5F4LLSL.js.map +0 -1
  69. package/dist/chunk-OW73RZOU.cjs +0 -313
  70. package/dist/chunk-OW73RZOU.cjs.map +0 -1
  71. package/dist/chunk-SVVKUMSL.cjs +0 -1733
  72. package/dist/chunk-SVVKUMSL.cjs.map +0 -1
  73. package/dist/chunk-TDGSY5W6.cjs +0 -474
  74. package/dist/chunk-TDGSY5W6.cjs.map +0 -1
  75. package/dist/chunk-WF3STNDX.cjs +0 -682
  76. package/dist/chunk-WF3STNDX.cjs.map +0 -1
  77. package/dist/chunk-XIRJGVDK.cjs +0 -1826
  78. package/dist/chunk-XIRJGVDK.cjs.map +0 -1
  79. package/dist/dashboard/index.cjs +0 -106
  80. package/dist/dashboard/index.cjs.map +0 -1
  81. package/dist/index.cjs +0 -650
  82. package/dist/index.cjs.map +0 -1
  83. package/dist/layout/index.cjs +0 -98
  84. package/dist/layout/index.cjs.map +0 -1
  85. package/dist/primitives/index.cjs +0 -363
  86. package/dist/primitives/index.cjs.map +0 -1
  87. package/dist/theme/index.cjs +0 -41
  88. package/dist/theme/index.cjs.map +0 -1
@@ -1,27 +1,8 @@
1
1
  import * as React from 'react'
2
- import {
3
- ResponsiveContainer,
4
- LineChart, Line,
5
- BarChart, Bar,
6
- AreaChart, Area,
7
- PieChart, Pie, Cell,
8
- XAxis, YAxis, CartesianGrid, Tooltip, Legend,
9
- } from 'recharts'
10
2
  import { Card, CardContent, CardHeader, CardTitle } from '../primitives/card'
11
3
  import { renderIcon } from './icon'
12
4
  import type { ChartSeries, ChartType, IconRef } from './types'
13
5
 
14
- /** Theme-driven palette (HSL triplet CSS vars defined in styles.css). */
15
- const PALETTE = [
16
- 'hsl(var(--info))',
17
- 'hsl(var(--magic))',
18
- 'hsl(var(--success))',
19
- 'hsl(var(--warning))',
20
- 'hsl(var(--destructive))',
21
- ]
22
- const AXIS = 'hsl(var(--muted-foreground))'
23
- const GRID = 'hsl(var(--border))'
24
-
25
6
  export interface ChartWidgetProps {
26
7
  type: ChartType
27
8
  /** Rows of data; each key referenced by a series.dataKey. */
@@ -37,12 +18,15 @@ export interface ChartWidgetProps {
37
18
  className?: string
38
19
  }
39
20
 
21
+ // The recharts body is lazy so recharts ships as an async chunk instead of in
22
+ // every consumer's entry bundle; the Card shell renders immediately and the
23
+ // fallback reserves the chart's height to avoid layout shift.
24
+ const ChartWidgetInner = React.lazy(() => import('./ChartWidgetInner'))
25
+
40
26
  /** recharts-backed chart in a Card. Replaces the hand-rolled CSS bar charts. */
41
27
  export function ChartWidget({
42
28
  type, data, series, categoryKey = 'name', title, icon, headerAction, height = 240, className,
43
29
  }: ChartWidgetProps) {
44
- const color = (i: number, explicit?: string) => explicit ?? PALETTE[i % PALETTE.length]
45
-
46
30
  return (
47
31
  <Card className={className}>
48
32
  {(title || headerAction) && (
@@ -57,50 +41,9 @@ export function ChartWidget({
57
41
  </CardHeader>
58
42
  )}
59
43
  <CardContent className="p-4 pt-2">
60
- <ResponsiveContainer width="100%" height={height}>
61
- {type === 'pie' ? (
62
- <PieChart>
63
- <Tooltip />
64
- <Pie data={data} dataKey={series[0]?.dataKey ?? 'value'} nameKey={categoryKey} innerRadius={50} outerRadius={80} paddingAngle={2}>
65
- {data.map((_, i) => <Cell key={i} fill={color(i, series[0]?.color)} />)}
66
- </Pie>
67
- <Legend />
68
- </PieChart>
69
- ) : type === 'bar' ? (
70
- <BarChart data={data}>
71
- <CartesianGrid strokeDasharray="3 3" stroke={GRID} vertical={false} />
72
- <XAxis dataKey={categoryKey} stroke={AXIS} fontSize={12} tickLine={false} axisLine={false} />
73
- <YAxis stroke={AXIS} fontSize={12} tickLine={false} axisLine={false} width={36} />
74
- <Tooltip cursor={{ fill: GRID, opacity: 0.3 }} />
75
- {series.length > 1 && <Legend />}
76
- {series.map((s, i) => (
77
- <Bar key={s.dataKey} dataKey={s.dataKey} name={s.label ?? s.dataKey} fill={color(i, s.color)} radius={[4, 4, 0, 0]} />
78
- ))}
79
- </BarChart>
80
- ) : type === 'area' ? (
81
- <AreaChart data={data}>
82
- <CartesianGrid strokeDasharray="3 3" stroke={GRID} vertical={false} />
83
- <XAxis dataKey={categoryKey} stroke={AXIS} fontSize={12} tickLine={false} axisLine={false} />
84
- <YAxis stroke={AXIS} fontSize={12} tickLine={false} axisLine={false} width={36} />
85
- <Tooltip />
86
- {series.length > 1 && <Legend />}
87
- {series.map((s, i) => (
88
- <Area key={s.dataKey} dataKey={s.dataKey} name={s.label ?? s.dataKey} stroke={color(i, s.color)} fill={color(i, s.color)} fillOpacity={0.15} />
89
- ))}
90
- </AreaChart>
91
- ) : (
92
- <LineChart data={data}>
93
- <CartesianGrid strokeDasharray="3 3" stroke={GRID} vertical={false} />
94
- <XAxis dataKey={categoryKey} stroke={AXIS} fontSize={12} tickLine={false} axisLine={false} />
95
- <YAxis stroke={AXIS} fontSize={12} tickLine={false} axisLine={false} width={36} />
96
- <Tooltip />
97
- {series.length > 1 && <Legend />}
98
- {series.map((s, i) => (
99
- <Line key={s.dataKey} dataKey={s.dataKey} name={s.label ?? s.dataKey} stroke={color(i, s.color)} strokeWidth={2} dot={false} />
100
- ))}
101
- </LineChart>
102
- )}
103
- </ResponsiveContainer>
44
+ <React.Suspense fallback={<div style={{ height }} />}>
45
+ <ChartWidgetInner type={type} data={data} series={series} categoryKey={categoryKey} height={height} />
46
+ </React.Suspense>
104
47
  </CardContent>
105
48
  </Card>
106
49
  )
@@ -0,0 +1,83 @@
1
+ import * as React from 'react'
2
+ import {
3
+ ResponsiveContainer,
4
+ LineChart, Line,
5
+ BarChart, Bar,
6
+ AreaChart, Area,
7
+ PieChart, Pie, Cell,
8
+ XAxis, YAxis, CartesianGrid, Tooltip, Legend,
9
+ } from 'recharts'
10
+ import type { ChartSeries, ChartType } from './types'
11
+
12
+ /** Theme-driven palette (HSL triplet CSS vars defined in styles.css). */
13
+ const PALETTE = [
14
+ 'hsl(var(--info))',
15
+ 'hsl(var(--magic))',
16
+ 'hsl(var(--success))',
17
+ 'hsl(var(--warning))',
18
+ 'hsl(var(--destructive))',
19
+ ]
20
+ const AXIS = 'hsl(var(--muted-foreground))'
21
+ const GRID = 'hsl(var(--border))'
22
+
23
+ export interface ChartWidgetInnerProps {
24
+ type: ChartType
25
+ data: Array<Record<string, string | number>>
26
+ series: ChartSeries[]
27
+ categoryKey: string
28
+ height: number
29
+ }
30
+
31
+ // recharts-backed chart body. Default export + own module so ChartWidget can
32
+ // React.lazy it — recharts (~400 KB min) then lives in an async chunk instead
33
+ // of every consumer's entry bundle.
34
+ export default function ChartWidgetInner({ type, data, series, categoryKey, height }: ChartWidgetInnerProps) {
35
+ const color = (i: number, explicit?: string) => explicit ?? PALETTE[i % PALETTE.length]
36
+
37
+ return (
38
+ <ResponsiveContainer width="100%" height={height}>
39
+ {type === 'pie' ? (
40
+ <PieChart>
41
+ <Tooltip />
42
+ <Pie data={data} dataKey={series[0]?.dataKey ?? 'value'} nameKey={categoryKey} innerRadius={50} outerRadius={80} paddingAngle={2}>
43
+ {data.map((_, i) => <Cell key={i} fill={color(i, series[0]?.color)} />)}
44
+ </Pie>
45
+ <Legend />
46
+ </PieChart>
47
+ ) : type === 'bar' ? (
48
+ <BarChart data={data}>
49
+ <CartesianGrid strokeDasharray="3 3" stroke={GRID} vertical={false} />
50
+ <XAxis dataKey={categoryKey} stroke={AXIS} fontSize={12} tickLine={false} axisLine={false} />
51
+ <YAxis stroke={AXIS} fontSize={12} tickLine={false} axisLine={false} width={36} />
52
+ <Tooltip cursor={{ fill: GRID, opacity: 0.3 }} />
53
+ {series.length > 1 && <Legend />}
54
+ {series.map((s, i) => (
55
+ <Bar key={s.dataKey} dataKey={s.dataKey} name={s.label ?? s.dataKey} fill={color(i, s.color)} radius={[4, 4, 0, 0]} />
56
+ ))}
57
+ </BarChart>
58
+ ) : type === 'area' ? (
59
+ <AreaChart data={data}>
60
+ <CartesianGrid strokeDasharray="3 3" stroke={GRID} vertical={false} />
61
+ <XAxis dataKey={categoryKey} stroke={AXIS} fontSize={12} tickLine={false} axisLine={false} />
62
+ <YAxis stroke={AXIS} fontSize={12} tickLine={false} axisLine={false} width={36} />
63
+ <Tooltip />
64
+ {series.length > 1 && <Legend />}
65
+ {series.map((s, i) => (
66
+ <Area key={s.dataKey} dataKey={s.dataKey} name={s.label ?? s.dataKey} stroke={color(i, s.color)} fill={color(i, s.color)} fillOpacity={0.15} />
67
+ ))}
68
+ </AreaChart>
69
+ ) : (
70
+ <LineChart data={data}>
71
+ <CartesianGrid strokeDasharray="3 3" stroke={GRID} vertical={false} />
72
+ <XAxis dataKey={categoryKey} stroke={AXIS} fontSize={12} tickLine={false} axisLine={false} />
73
+ <YAxis stroke={AXIS} fontSize={12} tickLine={false} axisLine={false} width={36} />
74
+ <Tooltip />
75
+ {series.length > 1 && <Legend />}
76
+ {series.map((s, i) => (
77
+ <Line key={s.dataKey} dataKey={s.dataKey} name={s.label ?? s.dataKey} stroke={color(i, s.color)} strokeWidth={2} dot={false} />
78
+ ))}
79
+ </LineChart>
80
+ )}
81
+ </ResponsiveContainer>
82
+ )
83
+ }
@@ -3,6 +3,7 @@ import { Check, Circle } from 'lucide-react'
3
3
  import { cn } from '../utils/cn'
4
4
  import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '../primitives/card'
5
5
  import { Button } from '../primitives/button'
6
+ import { Skeleton } from '../primitives/skeleton'
6
7
  import { renderIcon } from './icon'
7
8
  import type { IconRef } from './types'
8
9
 
@@ -30,6 +31,33 @@ export interface OnboardingWidgetProps {
30
31
  className?: string
31
32
  }
32
33
 
34
+ /** Placeholder mirroring the widget's layout while the async checks resolve. */
35
+ function OnboardingSkeleton({ rows, className }: { rows: number; className?: string }) {
36
+ return (
37
+ <Card className={className} aria-busy="true">
38
+ <CardHeader>
39
+ <Skeleton className="h-5 w-40" />
40
+ <Skeleton className="mt-1.5 h-3.5 w-64 max-w-full" />
41
+ <div className="mt-2 flex items-center gap-3">
42
+ <Skeleton className="h-1.5 flex-1 rounded-full" />
43
+ <Skeleton className="h-3 w-8" />
44
+ </div>
45
+ </CardHeader>
46
+ <CardContent className="space-y-1 pt-0">
47
+ {Array.from({ length: Math.max(rows, 1) }).map((_, i) => (
48
+ <div key={i} className="flex items-center gap-3 px-2 py-2.5">
49
+ <Skeleton className="h-7 w-7 shrink-0 rounded-full" />
50
+ <div className="min-w-0 flex-1 space-y-1.5">
51
+ <Skeleton className="h-3.5 w-1/2" />
52
+ <Skeleton className="h-3 w-3/4" />
53
+ </div>
54
+ </div>
55
+ ))}
56
+ </CardContent>
57
+ </Card>
58
+ )
59
+ }
60
+
33
61
  /** Getting-started checklist ("first steps to set up") with progress. */
34
62
  export function OnboardingWidget({
35
63
  title = 'Getting started', subtitle, steps, onNavigate, hideWhenComplete = true, className,
@@ -58,7 +86,10 @@ export function OnboardingWidget({
58
86
 
59
87
  const doneCount = steps.filter((s) => completed[s.id]).length
60
88
  const allDone = doneCount === steps.length
61
- if (!resolved) return null
89
+ // Hold the card's real shape while the checks settle. Painting the resolved
90
+ // checklist here would flash 0/N at an already-onboarded tenant, and painting
91
+ // nothing collapsed the row and shifted the dashboard on resolve.
92
+ if (!resolved) return <OnboardingSkeleton rows={steps.length} className={className} />
62
93
  if (allDone && hideWhenComplete) return null
63
94
 
64
95
  const runAction = (action: OnboardingStepInput['action']) => {
@@ -1,12 +1,12 @@
1
1
  import * as React from 'react'
2
- import * as LucideIcons from 'lucide-react'
2
+ import { getLucideIcon } from '../icons'
3
3
  import type { IconRef } from './types'
4
4
 
5
5
  /** Resolve an IconRef (lucide name or component) to a renderable element. */
6
6
  export function renderIcon(icon: IconRef | undefined, className = 'h-4 w-4'): React.ReactNode {
7
7
  if (!icon) return null
8
8
  if (typeof icon === 'string') {
9
- const Comp = (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[icon]
9
+ const Comp = getLucideIcon(icon)
10
10
  return Comp ? React.createElement(Comp, { className }) : null
11
11
  }
12
12
  return React.createElement(icon, { className })
package/src/icons.ts ADDED
@@ -0,0 +1,66 @@
1
+ import type { LucideIcon } from 'lucide-react'
2
+ import {
3
+ Activity, AlertTriangle, Apple, ArrowDownCircle, ArrowDownRight, ArrowLeftRight, ArrowUpCircle, ArrowUpRight,
4
+ Award, BadgeDollarSign, Ban, Banknote, BarChart3, Bell, Bike, BookOpen,
5
+ BookOpenCheck, Box, Boxes, Briefcase, Building2, Calendar, CalendarCheck, CalendarCheck2,
6
+ CalendarClock, CalendarDays, CalendarPlus, CalendarX, Camera, Cat, Check, CheckCircle,
7
+ CheckSquare, ChefHat, ChevronsUpDown, Circle, CircleCheck, CircleDollarSign, Clapperboard, ClipboardList,
8
+ Clock, Clock3, Command, Compass, Contact, CreditCard, Disc3, Dog,
9
+ DollarSign, DoorOpen, Egg, ExternalLink, Eye, FileCheck2, FileDown,
10
+ FileText, Filter, Flame, FolderOpen, Globe, GraduationCap, Handshake, Heart,
11
+ Home, Image, Inbox, Instagram, Landmark, Layers, LayoutDashboard, LayoutTemplate,
12
+ Leaf, LeafyGreen, Link, List, ListChecks, ListMusic, ListPlus, Lock,
13
+ Mail, Map, MapPin, Megaphone, MessageCircle, MessageSquare, Mic, MoreHorizontal,
14
+ MousePointerClick, Music, Newspaper, Package, PackageCheck, Palette, PartyPopper, PawPrint,
15
+ Pencil, Percent, Phone, PieChart, Plug, Plus, PlusCircle, Puzzle, Radio,
16
+ Receipt, Repeat, RotateCcw, Ruler, Search, Send, Settings, Shield,
17
+ ShieldCheck, ShoppingBag, ShoppingCart, Sigma, SlidersHorizontal, Sparkles, Star, Tag,
18
+ Tags, Target, ToggleLeft, TreePalm, TrendingDown, TrendingUp, Trophy, Truck,
19
+ User, UserCheck, UserCog, UserPlus, UserX, Users, UsersRound, Utensils,
20
+ UtensilsCrossed, Wallet, Warehouse, Wheat, Workflow, Wrench, X, XCircle,
21
+ Zap,
22
+ } from 'lucide-react'
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Central lucide icon registry. The SDK's manifest-driven surfaces resolve
26
+ // icons by string name; a namespace import (`import * as LucideIcons`) defeats
27
+ // tree-shaking and drags all ~1,500 icons (~500 KB min) into every bundle, so
28
+ // the SDK ships this curated set instead — the union of every icon name the
29
+ // SDK, plugins, and app templates reference. Apps using names outside the set
30
+ // call registerIcons({ Name: Component }) with their own named imports.
31
+ // ---------------------------------------------------------------------------
32
+
33
+ const registry: Record<string, LucideIcon> = {
34
+ Activity, AlertTriangle, Apple, ArrowDownCircle, ArrowDownRight, ArrowLeftRight, ArrowUpCircle, ArrowUpRight,
35
+ Award, BadgeDollarSign, Ban, Banknote, BarChart3, Bell, Bike, BookOpen,
36
+ BookOpenCheck, Box, Boxes, Briefcase, Building2, Calendar, CalendarCheck, CalendarCheck2,
37
+ CalendarClock, CalendarDays, CalendarPlus, CalendarX, Camera, Cat, Check, CheckCircle,
38
+ CheckSquare, ChefHat, ChevronsUpDown, Circle, CircleCheck, CircleDollarSign, Clapperboard, ClipboardList,
39
+ Clock, Clock3, Command, Compass, Contact, CreditCard, Disc3, Dog,
40
+ DollarSign, DoorOpen, Egg, ExternalLink, Eye, FileCheck2, FileDown,
41
+ FileText, Filter, Flame, FolderOpen, Globe, GraduationCap, Handshake, Heart,
42
+ Home, Image, Inbox, Instagram, Landmark, Layers, LayoutDashboard, LayoutTemplate,
43
+ Leaf, LeafyGreen, Link, List, ListChecks, ListMusic, ListPlus, Lock,
44
+ Mail, Map, MapPin, Megaphone, MessageCircle, MessageSquare, Mic, MoreHorizontal,
45
+ MousePointerClick, Music, Newspaper, Package, PackageCheck, Palette, PartyPopper, PawPrint,
46
+ Pencil, Percent, Phone, PieChart, Plug, Plus, PlusCircle, Puzzle, Radio,
47
+ Receipt, Repeat, RotateCcw, Ruler, Search, Send, Settings, Shield,
48
+ ShieldCheck, ShoppingBag, ShoppingCart, Sigma, SlidersHorizontal, Sparkles, Star, Tag,
49
+ Tags, Target, ToggleLeft, TreePalm, TrendingDown, TrendingUp, Trophy, Truck,
50
+ User, UserCheck, UserCog, UserPlus, UserX, Users, UsersRound, Utensils,
51
+ UtensilsCrossed, Wallet, Warehouse, Wheat, Workflow, Wrench, X, XCircle,
52
+ Zap,
53
+ }
54
+
55
+ /** Add or override icons resolvable by string name (e.g. from app manifests). */
56
+ export function registerIcons(icons: Record<string, LucideIcon>): void {
57
+ Object.assign(registry, icons)
58
+ }
59
+
60
+ /** Resolve a lucide icon by name; undefined when not registered. */
61
+ export function getLucideIcon(name: string): LucideIcon | undefined {
62
+ return registry[name]
63
+ }
64
+
65
+ /** The registered icon map (live reference — do not mutate directly). */
66
+ export const ICON_MAP = registry
package/src/index.ts CHANGED
@@ -228,3 +228,7 @@ export {
228
228
  type ChartType,
229
229
  type ChartSeries,
230
230
  } from './dashboard'
231
+
232
+ // Central lucide icon registry — curated tree-shakable set + escape hatch for
233
+ // apps that need icon names outside it (registerIcons({ Name: Component })).
234
+ export { registerIcons, getLucideIcon } from './icons'
@@ -1,104 +1,10 @@
1
1
  import * as React from 'react'
2
2
  import * as Tooltip from '@radix-ui/react-tooltip'
3
3
  import {
4
- Crown,
5
- Home,
6
- Users,
7
- Settings,
8
- CreditCard,
9
- Bell,
10
- Calendar,
11
- CalendarClock,
12
- Package,
13
- Activity,
14
- BarChart3,
15
- FileText,
16
- Mail,
17
- PanelLeftClose,
18
- PanelLeft,
19
- Search,
20
- Shield,
21
- DollarSign,
22
- Megaphone,
23
- Receipt,
24
- ShoppingCart,
25
- ShoppingBag,
26
- Target,
27
- Wrench,
28
- ClipboardList,
29
- ListChecks,
30
- Briefcase,
31
- UserCog,
32
- BookOpen,
33
- BookOpenCheck,
34
- MessageCircle,
35
- Globe,
36
- Percent,
37
- Tag,
38
- Tags,
39
- Camera,
40
- UtensilsCrossed,
41
- MapPin,
42
- Map,
43
- Handshake,
44
- Contact,
45
- Building2,
46
- Filter,
47
- Plus,
48
- List,
49
- ListPlus,
50
- ChevronDown,
51
- Ban,
52
- Clock,
53
- Dog,
54
- Cat,
55
- PawPrint,
56
- Heart,
57
- FolderOpen,
58
- LayoutTemplate,
59
- LeafyGreen,
60
- Apple,
61
- Egg,
62
- Wheat,
63
- PartyPopper,
64
- Radio,
65
- CalendarDays,
66
- CalendarCheck2,
67
- CalendarX,
68
- BadgeDollarSign,
69
- Banknote,
70
- Landmark,
71
- CircleDollarSign,
72
- Layers,
73
- Music,
74
- Eye,
75
- ListMusic,
76
- Disc3,
77
- UsersRound,
78
- Mic,
79
- Zap,
80
- Award,
81
- LayoutDashboard,
82
- MessageSquare,
83
- Inbox,
84
- Star,
85
- TrendingUp,
86
- GraduationCap,
87
- Repeat,
88
- UserCheck,
89
- UserPlus,
90
- UserX,
91
- Workflow,
92
- Boxes,
93
- Clock3,
94
- FileCheck2,
95
- User,
96
- Palette,
97
- ShieldCheck,
98
- SlidersHorizontal,
99
- Puzzle,
4
+ Bell, ChevronDown, Crown, ExternalLink, Home, PanelLeft, PanelLeftClose, Search, Settings, User,
100
5
  type LucideIcon,
101
6
  } from 'lucide-react'
7
+ import { ICON_MAP } from '../icons'
102
8
  import { cn } from '../utils/cn'
103
9
  import { useLayoutStore } from '../stores/layout.store'
104
10
 
@@ -114,6 +20,10 @@ export interface NavigationItem {
114
20
  * not entitle the feature. The item stays visible with a small Crown badge
115
21
  * and, on click, leads to the plan's UpgradePrompt. */
116
22
  premium?: boolean
23
+ /** Sales-channel / external link: when set, the item opens this URL in a new
24
+ * tab instead of navigating an internal route. Rendered as an anchor with an
25
+ * external-link glyph. `route` is ignored for matching when `href` is set. */
26
+ href?: string
117
27
  children?: NavigationItem[]
118
28
  }
119
29
 
@@ -160,18 +70,8 @@ export interface SidebarProps {
160
70
  navDirection?: 'forward' | 'back'
161
71
  }
162
72
 
163
- const ICON_MAP: Record<string, LucideIcon> = {
164
- Home, Users, Settings, CreditCard, Bell, Calendar, CalendarClock, Package, Activity, BarChart3,
165
- FileText, Mail, DollarSign, Megaphone, ShoppingCart, Target, Wrench,
166
- Receipt, ShoppingBag, ClipboardList, ListChecks, Briefcase, UserCog, BookOpen, BookOpenCheck, MessageCircle, Globe,
167
- Percent, Tag, Tags, Camera, UtensilsCrossed, MapPin, Map, Handshake, Contact,
168
- Building2, Filter, Plus, List, ListPlus, Search, Shield, Ban, Clock, Dog, Cat, PawPrint, Heart, FolderOpen, LayoutTemplate, LeafyGreen, Apple, Egg, Wheat,
169
- PartyPopper, Radio, CalendarDays, CalendarCheck2, CalendarX, BadgeDollarSign, Banknote, Landmark, CircleDollarSign, Layers, Music, Eye, ListMusic, Disc3, UsersRound, Mic,
170
- Zap, Award, LayoutDashboard, MessageSquare, Inbox, Star, TrendingUp, UserCheck, UserPlus, UserX, Workflow, Boxes, Clock3, FileCheck2,
171
- GraduationCap, Repeat,
172
- User, Palette, ShieldCheck, SlidersHorizontal, Puzzle,
173
- }
174
-
73
+ // ICON_MAP now lives in ../icons (central curated registry, shared by every
74
+ // string-name icon lookup in the SDK); re-exported here for compat.
175
75
  export { ICON_MAP }
176
76
 
177
77
  /** Kept in sync with --saas-rail-duration in styles.css. */
@@ -199,33 +99,46 @@ function NavItem({
199
99
  onNavigate: (route: string) => void
200
100
  }) {
201
101
  const Icon = getIcon(item.icon)
102
+ const isExternal = !!item.href
103
+
104
+ const className = cn(
105
+ 'flex w-full items-center gap-3 rounded-md px-2.5 py-1.5 text-[13px] font-medium transition-colors',
106
+ 'hover:bg-sidebar-accent/60 hover:text-sidebar-accent-foreground',
107
+ isActive
108
+ ? 'bg-sidebar-accent font-semibold text-sidebar-accent-foreground shadow-sm'
109
+ : 'text-sidebar-foreground/80',
110
+ collapsed && 'justify-center px-2',
111
+ )
202
112
 
203
- const content = (
204
- <button
205
- onClick={() => onNavigate(item.route)}
206
- className={cn(
207
- 'flex w-full items-center gap-3 rounded-md px-2.5 py-1.5 text-[13px] font-medium transition-colors',
208
- 'hover:bg-sidebar-accent/60 hover:text-sidebar-accent-foreground',
209
- isActive
210
- ? 'bg-sidebar-accent font-semibold text-sidebar-accent-foreground shadow-sm'
211
- : 'text-sidebar-foreground/80',
212
- collapsed && 'justify-center px-2',
213
- )}
214
- >
113
+ const inner = (
114
+ <>
215
115
  <Icon className="h-4 w-4 shrink-0" />
216
116
  {!collapsed && (
217
117
  <>
218
118
  <span className="flex-1 truncate text-left">{item.label}</span>
119
+ {isExternal && (
120
+ <ExternalLink className="ml-auto h-3.5 w-3.5 shrink-0 opacity-50" aria-hidden />
121
+ )}
219
122
  {item.premium && (
220
- <Crown className="ml-auto h-3.5 w-3.5 shrink-0 text-amber-500" aria-hidden />
123
+ <Crown className={cn('h-3.5 w-3.5 shrink-0 text-amber-500', !isExternal && 'ml-auto')} aria-hidden />
221
124
  )}
222
125
  {item.badge !== undefined && (
223
- <span className="ml-auto rounded-full bg-primary px-2 py-0.5 text-xs font-semibold text-primary-foreground">
126
+ <span className={cn('rounded-full bg-primary px-2 py-0.5 text-xs font-semibold text-primary-foreground', !isExternal && !item.premium && 'ml-auto')}>
224
127
  {item.badge}
225
128
  </span>
226
129
  )}
227
130
  </>
228
131
  )}
132
+ </>
133
+ )
134
+
135
+ const content = isExternal ? (
136
+ <a href={item.href} target="_blank" rel="noopener noreferrer" className={className}>
137
+ {inner}
138
+ </a>
139
+ ) : (
140
+ <button onClick={() => onNavigate(item.route)} className={className}>
141
+ {inner}
229
142
  </button>
230
143
  )
231
144
 
@@ -342,19 +255,32 @@ function NavGroup({
342
255
  {item.children?.map((child) => {
343
256
  const ChildIcon = getIcon(child.icon)
344
257
  const isActive = child.route === activeRoute
345
- return (
346
- <button
347
- key={child.id}
348
- onClick={() => onNavigate(child.route)}
349
- className={cn(
350
- 'flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-sm transition-colors',
351
- isActive
352
- ? 'font-medium text-sidebar-accent-foreground'
353
- : 'text-sidebar-muted hover:text-sidebar-accent-foreground'
354
- )}
355
- >
258
+ const childExternal = !!child.href
259
+ const childClass = cn(
260
+ 'flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-sm transition-colors',
261
+ isActive
262
+ ? 'font-medium text-sidebar-accent-foreground'
263
+ : 'text-sidebar-muted hover:text-sidebar-accent-foreground'
264
+ )
265
+ const childInner = (
266
+ <>
356
267
  <ChildIcon className="h-3.5 w-3.5 shrink-0" />
357
- <span className="truncate">{child.label}</span>
268
+ <span className="flex-1 truncate text-left">{child.label}</span>
269
+ {childExternal && (
270
+ <ExternalLink className="h-3 w-3 shrink-0 opacity-50" aria-hidden />
271
+ )}
272
+ {child.premium && (
273
+ <Crown className="h-3 w-3 shrink-0 text-amber-500" aria-hidden />
274
+ )}
275
+ </>
276
+ )
277
+ return childExternal ? (
278
+ <a key={child.id} href={child.href} target="_blank" rel="noopener noreferrer" className={childClass}>
279
+ {childInner}
280
+ </a>
281
+ ) : (
282
+ <button key={child.id} onClick={() => onNavigate(child.route)} className={childClass}>
283
+ {childInner}
358
284
  </button>
359
285
  )
360
286
  })}
@@ -422,7 +348,13 @@ export function Sidebar({
422
348
  // node so a context change can snapshot the outgoing rail and animate it out.
423
349
  const navBody = (
424
350
  <>
425
- {topContent && <div className="mb-4 space-y-2">{topContent}</div>}
351
+ {topContent && (
352
+ <div className="mb-4 space-y-2">
353
+ {React.isValidElement(topContent)
354
+ ? React.cloneElement(topContent as React.ReactElement<any>, { collapsed })
355
+ : topContent}
356
+ </div>
357
+ )}
426
358
  {mainItems.length > 0 && (
427
359
  <div className="space-y-1">
428
360
  {sectionHeading(sectionLabels?.main)}
@@ -509,7 +441,9 @@ export function Sidebar({
509
441
  <nav
510
442
  className={cn(
511
443
  'relative flex-1 p-2',
512
- outgoing ? 'overflow-hidden' : 'overflow-y-auto',
444
+ // scrollbar-none: the rail still scrolls, but without the native
445
+ // scrollbar gutter eating into the nav items.
446
+ outgoing ? 'overflow-hidden' : 'overflow-y-auto scrollbar-none',
513
447
  )}
514
448
  >
515
449
  {outgoing && (
@@ -28,6 +28,17 @@ function formatDate(date: Date, locale: string) {
28
28
  return date.toLocaleDateString(toDateLocale(locale), { month: 'short', day: 'numeric', year: 'numeric' })
29
29
  }
30
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
+
31
42
  function toDateStr(d: Date) {
32
43
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
33
44
  }
@@ -42,13 +53,24 @@ interface DatePickerProps {
42
53
  value: string
43
54
  onChange: (value: string) => void
44
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'
45
62
  /** Open the dropdown immediately on mount */
46
63
  defaultOpen?: boolean
47
64
  /** Called when the dropdown opens or closes */
48
65
  onOpenChange?: (open: boolean) => void
49
66
  }
50
67
 
51
- export function DatePicker({ value, onChange, className, defaultOpen, onOpenChange }: DatePickerProps) {
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) {
52
74
  const t = useTranslation()
53
75
  const locale = getCurrentLocale()
54
76
  const [open, setOpen] = useState(defaultOpen ?? false)
@@ -129,9 +151,13 @@ export function DatePicker({ value, onChange, className, defaultOpen, onOpenChan
129
151
  <div ref={ref} className={`relative ${className ?? ''}`}>
130
152
  {/* Trigger */}
131
153
  <button ref={triggerRef} type="button" onClick={() => setOpen((p) => !p)}
132
- className="flex items-center gap-2 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm hover:bg-muted/30 transition-colors w-full text-left">
133
- <Calendar className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
134
- <span>{value ? formatDate(selectedDate, locale) : t('common.selectDate')}</span>
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>
135
161
  </button>
136
162
 
137
163
  {/* Dropdown — portal to escape overflow containers */}