@fayz-ai/ui 0.8.3 → 0.8.4

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 (50) hide show
  1. package/dist/{chunk-XIRJGVDK.cjs → chunk-7ZQQZD4Q.cjs} +34 -37
  2. package/dist/{chunk-ID2HQK2Q.js.map → chunk-7ZQQZD4Q.cjs.map} +1 -1
  3. package/dist/{chunk-SVVKUMSL.cjs → chunk-B3BTZDTA.cjs} +10 -42
  4. package/dist/chunk-B3BTZDTA.cjs.map +1 -0
  5. package/dist/{chunk-O5F4LLSL.js → chunk-DYMBFHD7.js} +8 -7
  6. package/dist/chunk-DYMBFHD7.js.map +1 -0
  7. package/dist/{chunk-BVF2KPTT.js → chunk-G6OKZ7WL.js} +30 -3
  8. package/dist/chunk-G6OKZ7WL.js.map +1 -0
  9. package/dist/{chunk-A7O7KGNL.js → chunk-M34ACV5E.js} +23 -4
  10. package/dist/chunk-M34ACV5E.js.map +1 -0
  11. package/dist/{chunk-WF3STNDX.cjs → chunk-MUGRP5DD.cjs} +53 -34
  12. package/dist/chunk-MUGRP5DD.cjs.map +1 -0
  13. package/dist/{chunk-MI6FOB4O.cjs → chunk-NZV47HWO.cjs} +31 -2
  14. package/dist/chunk-NZV47HWO.cjs.map +1 -0
  15. package/dist/{chunk-2GDQUARM.js → chunk-R4LPSXTT.js} +8 -38
  16. package/dist/chunk-R4LPSXTT.js.map +1 -0
  17. package/dist/{chunk-TDGSY5W6.cjs → chunk-R4SQ2GVD.cjs} +8 -7
  18. package/dist/chunk-R4SQ2GVD.cjs.map +1 -0
  19. package/dist/{chunk-ID2HQK2Q.js → chunk-VVQ45ZDZ.js} +35 -38
  20. package/dist/chunk-VVQ45ZDZ.js.map +1 -0
  21. package/dist/dashboard/OnboardingWidget.d.ts.map +1 -1
  22. package/dist/dashboard/index.cjs +26 -26
  23. package/dist/dashboard/index.js +2 -2
  24. package/dist/index.cjs +137 -137
  25. package/dist/index.js +5 -5
  26. package/dist/layout/Sidebar.d.ts +4 -0
  27. package/dist/layout/Sidebar.d.ts.map +1 -1
  28. package/dist/layout/index.cjs +19 -19
  29. package/dist/layout/index.js +1 -1
  30. package/dist/primitives/index.cjs +83 -83
  31. package/dist/primitives/index.js +2 -2
  32. package/dist/primitives/search-select.d.ts +3 -1
  33. package/dist/primitives/search-select.d.ts.map +1 -1
  34. package/dist/theme/index.cjs +8 -8
  35. package/dist/theme/index.d.ts.map +1 -1
  36. package/dist/theme/index.js +1 -1
  37. package/package.json +3 -3
  38. package/src/dashboard/OnboardingWidget.tsx +32 -1
  39. package/src/layout/Sidebar.tsx +69 -29
  40. package/src/primitives/search-select.tsx +12 -4
  41. package/src/theme/index.ts +9 -6
  42. package/dist/chunk-2GDQUARM.js.map +0 -1
  43. package/dist/chunk-A7O7KGNL.js.map +0 -1
  44. package/dist/chunk-BVF2KPTT.js.map +0 -1
  45. package/dist/chunk-MI6FOB4O.cjs.map +0 -1
  46. package/dist/chunk-O5F4LLSL.js.map +0 -1
  47. package/dist/chunk-SVVKUMSL.cjs.map +0 -1
  48. package/dist/chunk-TDGSY5W6.cjs.map +0 -1
  49. package/dist/chunk-WF3STNDX.cjs.map +0 -1
  50. package/dist/chunk-XIRJGVDK.cjs.map +0 -1
@@ -1,4 +1,4 @@
1
- export { ThemeProvider, createFayzTheme, createTheme, darkTheme, fayzThemePresets, lightTheme, useTheme } from '../chunk-O5F4LLSL.js';
1
+ export { ThemeProvider, createFayzTheme, createTheme, darkTheme, fayzThemePresets, lightTheme, useTheme } from '../chunk-DYMBFHD7.js';
2
2
  export { fayzUiPreset } from '../chunk-IWBXYV4U.js';
3
3
  //# sourceMappingURL=index.js.map
4
4
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "fayz": {
4
4
  "status": "beta"
5
5
  },
6
- "version": "0.8.3",
6
+ "version": "0.8.4",
7
7
  "description": "Fayz SDK UI — Radix + Tailwind component library with CRUD engine and layout shells",
8
8
  "type": "module",
9
9
  "main": "./dist/index.cjs",
@@ -84,8 +84,8 @@
84
84
  "sonner": "^1.5.0",
85
85
  "tailwind-merge": "^2.4.0",
86
86
  "zustand": "^4.5.0",
87
- "@fayz-ai/core": "^0.8.3",
88
- "@fayz-ai/auth": "^0.8.3"
87
+ "@fayz-ai/auth": "^0.8.4",
88
+ "@fayz-ai/core": "^0.8.4"
89
89
  },
90
90
  "devDependencies": {
91
91
  "@types/react": "^18.3.0",
@@ -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']) => {
@@ -2,6 +2,7 @@ import * as React from 'react'
2
2
  import * as Tooltip from '@radix-ui/react-tooltip'
3
3
  import {
4
4
  Crown,
5
+ ExternalLink,
5
6
  Home,
6
7
  Users,
7
8
  Settings,
@@ -25,6 +26,7 @@ import {
25
26
  ShoppingBag,
26
27
  Target,
27
28
  Wrench,
29
+ Truck,
28
30
  ClipboardList,
29
31
  ListChecks,
30
32
  Briefcase,
@@ -114,6 +116,10 @@ export interface NavigationItem {
114
116
  * not entitle the feature. The item stays visible with a small Crown badge
115
117
  * and, on click, leads to the plan's UpgradePrompt. */
116
118
  premium?: boolean
119
+ /** Sales-channel / external link: when set, the item opens this URL in a new
120
+ * tab instead of navigating an internal route. Rendered as an anchor with an
121
+ * external-link glyph. `route` is ignored for matching when `href` is set. */
122
+ href?: string
117
123
  children?: NavigationItem[]
118
124
  }
119
125
 
@@ -162,7 +168,7 @@ export interface SidebarProps {
162
168
 
163
169
  const ICON_MAP: Record<string, LucideIcon> = {
164
170
  Home, Users, Settings, CreditCard, Bell, Calendar, CalendarClock, Package, Activity, BarChart3,
165
- FileText, Mail, DollarSign, Megaphone, ShoppingCart, Target, Wrench,
171
+ FileText, Mail, DollarSign, Megaphone, ShoppingCart, Target, Wrench, Truck,
166
172
  Receipt, ShoppingBag, ClipboardList, ListChecks, Briefcase, UserCog, BookOpen, BookOpenCheck, MessageCircle, Globe,
167
173
  Percent, Tag, Tags, Camera, UtensilsCrossed, MapPin, Map, Handshake, Contact,
168
174
  Building2, Filter, Plus, List, ListPlus, Search, Shield, Ban, Clock, Dog, Cat, PawPrint, Heart, FolderOpen, LayoutTemplate, LeafyGreen, Apple, Egg, Wheat,
@@ -199,33 +205,46 @@ function NavItem({
199
205
  onNavigate: (route: string) => void
200
206
  }) {
201
207
  const Icon = getIcon(item.icon)
208
+ const isExternal = !!item.href
209
+
210
+ const className = cn(
211
+ 'flex w-full items-center gap-3 rounded-md px-2.5 py-1.5 text-[13px] font-medium transition-colors',
212
+ 'hover:bg-sidebar-accent/60 hover:text-sidebar-accent-foreground',
213
+ isActive
214
+ ? 'bg-sidebar-accent font-semibold text-sidebar-accent-foreground shadow-sm'
215
+ : 'text-sidebar-foreground/80',
216
+ collapsed && 'justify-center px-2',
217
+ )
202
218
 
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
- >
219
+ const inner = (
220
+ <>
215
221
  <Icon className="h-4 w-4 shrink-0" />
216
222
  {!collapsed && (
217
223
  <>
218
224
  <span className="flex-1 truncate text-left">{item.label}</span>
225
+ {isExternal && (
226
+ <ExternalLink className="ml-auto h-3.5 w-3.5 shrink-0 opacity-50" aria-hidden />
227
+ )}
219
228
  {item.premium && (
220
- <Crown className="ml-auto h-3.5 w-3.5 shrink-0 text-amber-500" aria-hidden />
229
+ <Crown className={cn('h-3.5 w-3.5 shrink-0 text-amber-500', !isExternal && 'ml-auto')} aria-hidden />
221
230
  )}
222
231
  {item.badge !== undefined && (
223
- <span className="ml-auto rounded-full bg-primary px-2 py-0.5 text-xs font-semibold text-primary-foreground">
232
+ <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
233
  {item.badge}
225
234
  </span>
226
235
  )}
227
236
  </>
228
237
  )}
238
+ </>
239
+ )
240
+
241
+ const content = isExternal ? (
242
+ <a href={item.href} target="_blank" rel="noopener noreferrer" className={className}>
243
+ {inner}
244
+ </a>
245
+ ) : (
246
+ <button onClick={() => onNavigate(item.route)} className={className}>
247
+ {inner}
229
248
  </button>
230
249
  )
231
250
 
@@ -342,19 +361,32 @@ function NavGroup({
342
361
  {item.children?.map((child) => {
343
362
  const ChildIcon = getIcon(child.icon)
344
363
  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
- >
364
+ const childExternal = !!child.href
365
+ const childClass = cn(
366
+ 'flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-sm transition-colors',
367
+ isActive
368
+ ? 'font-medium text-sidebar-accent-foreground'
369
+ : 'text-sidebar-muted hover:text-sidebar-accent-foreground'
370
+ )
371
+ const childInner = (
372
+ <>
356
373
  <ChildIcon className="h-3.5 w-3.5 shrink-0" />
357
- <span className="truncate">{child.label}</span>
374
+ <span className="flex-1 truncate text-left">{child.label}</span>
375
+ {childExternal && (
376
+ <ExternalLink className="h-3 w-3 shrink-0 opacity-50" aria-hidden />
377
+ )}
378
+ {child.premium && (
379
+ <Crown className="h-3 w-3 shrink-0 text-amber-500" aria-hidden />
380
+ )}
381
+ </>
382
+ )
383
+ return childExternal ? (
384
+ <a key={child.id} href={child.href} target="_blank" rel="noopener noreferrer" className={childClass}>
385
+ {childInner}
386
+ </a>
387
+ ) : (
388
+ <button key={child.id} onClick={() => onNavigate(child.route)} className={childClass}>
389
+ {childInner}
358
390
  </button>
359
391
  )
360
392
  })}
@@ -422,7 +454,13 @@ export function Sidebar({
422
454
  // node so a context change can snapshot the outgoing rail and animate it out.
423
455
  const navBody = (
424
456
  <>
425
- {topContent && <div className="mb-4 space-y-2">{topContent}</div>}
457
+ {topContent && (
458
+ <div className="mb-4 space-y-2">
459
+ {React.isValidElement(topContent)
460
+ ? React.cloneElement(topContent as React.ReactElement<any>, { collapsed })
461
+ : topContent}
462
+ </div>
463
+ )}
426
464
  {mainItems.length > 0 && (
427
465
  <div className="space-y-1">
428
466
  {sectionHeading(sectionLabels?.main)}
@@ -509,7 +547,9 @@ export function Sidebar({
509
547
  <nav
510
548
  className={cn(
511
549
  'relative flex-1 p-2',
512
- outgoing ? 'overflow-hidden' : 'overflow-y-auto',
550
+ // scrollbar-none: the rail still scrolls, but without the native
551
+ // scrollbar gutter eating into the nav items.
552
+ outgoing ? 'overflow-hidden' : 'overflow-y-auto scrollbar-none',
513
553
  )}
514
554
  >
515
555
  {outgoing && (
@@ -43,7 +43,9 @@ export interface SearchSelectProps {
43
43
  onCreate?: (query: string) => void | Promise<void>
44
44
  /** Custom render for each option */
45
45
  renderOption?: (option: SearchSelectOption) => React.ReactNode
46
- /** Load initial results when dropdown opens (calls onSearch('')) */
46
+ /** Load initial results when dropdown opens (calls onSearch('')). Default
47
+ * true — clicking lists options before any typing; pass false to require
48
+ * a query first (e.g. very large datasets with no cheap top-N). */
47
49
  loadOnOpen?: boolean
48
50
  /** Additional className for the container */
49
51
  className?: string
@@ -69,7 +71,7 @@ export function SearchSelect({
69
71
  createLabel = 'Add',
70
72
  onCreate,
71
73
  renderOption,
72
- loadOnOpen,
74
+ loadOnOpen = true,
73
75
  className,
74
76
  disabled,
75
77
  }: SearchSelectProps) {
@@ -162,6 +164,10 @@ export function SearchSelect({
162
164
  if (disabled || creating) return
163
165
  setOpen(true)
164
166
  setQuery('')
167
+ // Stale results from a previous typed query would render for the empty
168
+ // query now that the dropdown shows immediately — drop them so the
169
+ // loadOnOpen effect refetches the initial listing.
170
+ setResults([])
165
171
  setTimeout(() => inputRef.current?.focus(), 0)
166
172
  }
167
173
 
@@ -177,7 +183,9 @@ export function SearchSelect({
177
183
  }, [results])
178
184
 
179
185
  const hasValue = !!value && !!selectedLabel
180
- const showDropdown = open && query.length >= minChars
186
+ // With loadOnOpen the dropdown must show for the EMPTY query too — gating it
187
+ // on minChars silently hid the initial listing loadOnOpen had just fetched.
188
+ const showDropdown = open && (query.length >= minChars || loadOnOpen)
181
189
 
182
190
  return (
183
191
  <div ref={containerRef} className={cn('relative', className)}>
@@ -246,7 +254,7 @@ export function SearchSelect({
246
254
  </div>
247
255
  ) : results.length === 0 ? (
248
256
  <div className="px-4 py-4 text-center">
249
- <p className="text-xs text-muted-foreground">No results for "{query}"</p>
257
+ <p className="text-xs text-muted-foreground">{query ? `No results for "${query}"` : 'No results'}</p>
250
258
  {allowCreate && onCreate && (
251
259
  <button
252
260
  onClick={() => handleCreate(query)}
@@ -60,16 +60,19 @@ export function useTheme() {
60
60
  export function createTheme(overrides: CreateThemeOptions): ThemeTokens {
61
61
  const colors = { ...lightTheme.colors, ...overrides.colors }
62
62
 
63
- // Brand shorthand: auto-derive primary, ring, accent from single HSL
63
+ // Brand shorthand: auto-derive primary, ring, accent from single HSL.
64
+ // Explicit `colors.*` overrides win — the shorthand is a default, not a
65
+ // mandate, so an app can pin any one of these without dropping the rest.
64
66
  if (overrides.brand) {
65
- colors.primary = overrides.brand
66
- colors.primaryForeground = '0 0% 100%'
67
- colors.ring = overrides.brand
67
+ const given = overrides.colors ?? {}
68
+ if (given.primary == null) colors.primary = overrides.brand
69
+ if (given.primaryForeground == null) colors.primaryForeground = '0 0% 100%'
70
+ if (given.ring == null) colors.ring = overrides.brand
68
71
  const parts = overrides.brand.split(' ')
69
72
  if (parts.length >= 3) {
70
73
  const hue = (parseFloat(parts[0]) - 50 + 360) % 360
71
- colors.accent = `${hue} ${parts[1]} ${parts[2]}`
72
- colors.accentForeground = '0 0% 100%'
74
+ if (given.accent == null) colors.accent = `${hue} ${parts[1]} ${parts[2]}`
75
+ if (given.accentForeground == null) colors.accentForeground = '0 0% 100%'
73
76
  }
74
77
  }
75
78