@a4ui/core 0.37.0 → 0.38.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 (105) hide show
  1. package/README.md +8 -7
  2. package/dist/Collapse-Cr5cDqAl.js +375 -0
  3. package/dist/charts/AreaChart.d.ts +38 -0
  4. package/dist/charts/BarChart.d.ts +17 -1
  5. package/dist/charts/PieChart.d.ts +29 -0
  6. package/dist/charts/RadialBarChart.d.ts +33 -0
  7. package/dist/charts/index.d.ts +5 -0
  8. package/dist/charts/shared.d.ts +111 -0
  9. package/dist/charts.js +1493 -780
  10. package/dist/commerce.js +625 -634
  11. package/dist/elements.css +1076 -8
  12. package/dist/elements.iife.js +1 -6
  13. package/dist/elements.js +7774 -9133
  14. package/dist/full.css +1076 -8
  15. package/dist/index.d.ts +30 -3
  16. package/dist/index.js +17502 -15098
  17. package/dist/layout/Aurora.d.ts +16 -0
  18. package/dist/layout/EffectsToggle.d.ts +1 -1
  19. package/dist/layout/SpaceBackground.d.ts +1 -1
  20. package/dist/lib/anchored.d.ts +16 -0
  21. package/dist/motion-CTIBwMCA.js +104 -0
  22. package/dist/styles.css +110 -3
  23. package/dist/ui/AspectRatio.d.ts +19 -0
  24. package/dist/ui/Attachment.d.ts +70 -0
  25. package/dist/ui/ButtonGroup.d.ts +37 -0
  26. package/dist/ui/ChatMarker.d.ts +20 -0
  27. package/dist/ui/ChatThread.d.ts +32 -8
  28. package/dist/ui/ContactSection.d.ts +49 -0
  29. package/dist/ui/CtaBanner.d.ts +28 -0
  30. package/dist/ui/DateField.d.ts +2 -2
  31. package/dist/ui/Drawer.d.ts +5 -2
  32. package/dist/ui/Faq.d.ts +29 -0
  33. package/dist/ui/FeatureGrid.d.ts +35 -0
  34. package/dist/ui/Field.d.ts +148 -0
  35. package/dist/ui/Hero.d.ts +48 -0
  36. package/dist/ui/InputGroup.d.ts +93 -0
  37. package/dist/ui/Item.d.ts +63 -0
  38. package/dist/ui/Label.d.ts +23 -0
  39. package/dist/ui/Menubar.d.ts +83 -0
  40. package/dist/ui/Message.d.ts +51 -5
  41. package/dist/ui/Modal.d.ts +8 -2
  42. package/dist/ui/NativeSelect.d.ts +26 -0
  43. package/dist/ui/NavigationMenu.d.ts +85 -0
  44. package/dist/ui/PromptComposer.d.ts +5 -0
  45. package/dist/ui/Prose.d.ts +21 -0
  46. package/dist/ui/Questionnaire.d.ts +71 -0
  47. package/dist/ui/ScrollArea.d.ts +30 -0
  48. package/dist/ui/Sidebar.d.ts +229 -0
  49. package/dist/ui/SiteFooter.d.ts +39 -0
  50. package/dist/ui/SiteNav.d.ts +35 -0
  51. package/dist/ui/TeamGrid.d.ts +33 -0
  52. package/dist/ui/Testimonials.d.ts +37 -0
  53. package/dist/ui/internal/sectionKit.d.ts +22 -0
  54. package/dist/ui/internal/socialIcons.d.ts +9 -0
  55. package/package.json +38 -26
  56. package/preset.d.ts +23 -0
  57. package/src/charts/AreaChart.tsx +206 -0
  58. package/src/charts/BarChart.tsx +241 -5
  59. package/src/charts/DonutChart.tsx +8 -11
  60. package/src/charts/LineChart.tsx +2 -3
  61. package/src/charts/PieChart.tsx +145 -0
  62. package/src/charts/RadialBarChart.tsx +141 -0
  63. package/src/charts/index.ts +14 -0
  64. package/src/charts/shared.tsx +304 -0
  65. package/src/index.ts +192 -3
  66. package/src/layout/Aurora.tsx +36 -7
  67. package/src/layout/sceneEffects.ts +22 -3
  68. package/src/lib/anchored.ts +57 -0
  69. package/src/ui/AspectRatio.tsx +35 -0
  70. package/src/ui/Attachment.tsx +189 -0
  71. package/src/ui/ButtonGroup.tsx +89 -0
  72. package/src/ui/ChatMarker.tsx +58 -0
  73. package/src/ui/ChatThread.tsx +115 -23
  74. package/src/ui/ContactSection.tsx +199 -0
  75. package/src/ui/CtaBanner.tsx +62 -0
  76. package/src/ui/DateField.tsx +17 -7
  77. package/src/ui/DateRangePicker.tsx +15 -5
  78. package/src/ui/Drawer.tsx +37 -4
  79. package/src/ui/Faq.tsx +57 -0
  80. package/src/ui/FeatureGrid.tsx +77 -0
  81. package/src/ui/Field.tsx +374 -0
  82. package/src/ui/Hero.tsx +146 -0
  83. package/src/ui/InputGroup.tsx +219 -0
  84. package/src/ui/Item.tsx +185 -0
  85. package/src/ui/Label.tsx +50 -0
  86. package/src/ui/Menubar.tsx +212 -0
  87. package/src/ui/Message.tsx +139 -13
  88. package/src/ui/Modal.tsx +41 -5
  89. package/src/ui/MultiSelect.tsx +15 -5
  90. package/src/ui/NativeSelect.tsx +66 -0
  91. package/src/ui/NavigationMenu.tsx +262 -0
  92. package/src/ui/PromptComposer.tsx +11 -0
  93. package/src/ui/Prose.tsx +63 -0
  94. package/src/ui/Questionnaire.tsx +331 -0
  95. package/src/ui/ScrollArea.tsx +70 -0
  96. package/src/ui/Sidebar.tsx +753 -0
  97. package/src/ui/SiteFooter.tsx +112 -0
  98. package/src/ui/SiteNav.tsx +146 -0
  99. package/src/ui/TeamGrid.tsx +81 -0
  100. package/src/ui/Testimonials.tsx +103 -0
  101. package/src/ui/TimeField.tsx +15 -5
  102. package/src/ui/internal/sectionKit.tsx +72 -0
  103. package/src/ui/internal/socialIcons.tsx +41 -0
  104. package/dist/NumberInput-Bnh2u7pp.js +0 -381
  105. package/dist/motion-jYMWmZqB.js +0 -119
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  // import '@a4ui/core/styles.css'
9
9
  // import { Button, Card, Modal } from '@a4ui/core'
10
10
 
11
- export const A4UI_VERSION = '0.37.0'
11
+ export const A4UI_VERSION = '0.38.0'
12
12
 
13
13
  // Helpers (src/lib) — generic, framework-level utilities.
14
14
  export { cn } from './lib/cn'
@@ -42,6 +42,19 @@ export { createLinkedSelection, type LinkedSelection } from './lib/createLinkedS
42
42
  export { Accordion, type AccordionItem } from './ui/Accordion'
43
43
  export { Badge, type BadgeTone } from './ui/Badge'
44
44
  export { Button, type ButtonVariant } from './ui/Button'
45
+ export {
46
+ ButtonGroup,
47
+ ButtonGroupText,
48
+ ButtonGroupSeparator,
49
+ type ButtonGroupProps,
50
+ type ButtonGroupTextProps,
51
+ type ButtonGroupSeparatorProps,
52
+ type ButtonGroupOrientation,
53
+ } from './ui/ButtonGroup'
54
+ export { AspectRatio, type AspectRatioProps } from './ui/AspectRatio'
55
+ export { Label, type LabelProps } from './ui/Label'
56
+ export { NativeSelect, type NativeSelectProps, type NativeSelectSize } from './ui/NativeSelect'
57
+ export { Prose, type ProseProps, type ProseSize } from './ui/Prose'
45
58
  export { Card, CardHeader, CardTitle, CardContent } from './ui/Card'
46
59
  export { Checkbox } from './ui/Checkbox'
47
60
  export { DateField } from './ui/DateField'
@@ -49,12 +62,55 @@ export { TimeField, type TimeFieldProps } from './ui/TimeField'
49
62
  export { DateTimeField, type DateTimeFieldProps } from './ui/DateTimeField'
50
63
  export { DateRangePicker, type DateRange, type DateRangePickerProps } from './ui/DateRangePicker'
51
64
  export { FormField, FormLabel, FormControl, FormDescription, FormError, type FormFieldProps } from './ui/Form'
65
+ export {
66
+ Field,
67
+ FieldSet,
68
+ FieldLegend,
69
+ FieldGroup,
70
+ FieldContent,
71
+ FieldLabel,
72
+ FieldTitle,
73
+ FieldDescription,
74
+ FieldSeparator,
75
+ FieldError,
76
+ type FieldProps,
77
+ type FieldSetProps,
78
+ type FieldLegendProps,
79
+ type FieldGroupProps,
80
+ type FieldLabelProps,
81
+ type FieldSeparatorProps,
82
+ type FieldErrorProps,
83
+ type FieldOrientation,
84
+ } from './ui/Field'
85
+ export {
86
+ InputGroup,
87
+ InputGroupAddon,
88
+ InputGroupText,
89
+ InputGroupButton,
90
+ InputGroupInput,
91
+ InputGroupTextarea,
92
+ type InputGroupProps,
93
+ type InputGroupAddonProps,
94
+ type InputGroupAddonAlign,
95
+ type InputGroupButtonProps,
96
+ type InputGroupButtonSize,
97
+ type InputGroupInputProps,
98
+ type InputGroupTextareaProps,
99
+ } from './ui/InputGroup'
52
100
  export { FileUpload, type UploadFile, type FileUploadProps } from './ui/FileUpload'
53
101
  export { MultiSelect, type MultiSelectOption, type MultiSelectProps } from './ui/MultiSelect'
54
- export { Drawer } from './ui/Drawer'
102
+ export { Drawer, type DrawerProps, type DrawerSide } from './ui/Drawer'
55
103
  export { Dropdown, type DropdownItem } from './ui/Dropdown'
56
104
  export { Dropzone } from './ui/Dropzone'
57
105
  export { Input } from './ui/Input'
106
+ export {
107
+ Questionnaire,
108
+ type QuestionnaireProps,
109
+ type QuestionnaireQuestion,
110
+ type QuestionnaireOption,
111
+ type QuestionnaireAnswers,
112
+ type QuestionnaireLabels,
113
+ } from './ui/Questionnaire'
58
114
  export { Modal } from './ui/Modal'
59
115
  export { PageHeader } from './ui/PageHeader'
60
116
  export { Pagination } from './ui/Pagination'
@@ -84,6 +140,37 @@ export { Toggle } from './ui/Toggle'
84
140
  export { ToggleGroup, type ToggleGroupOption } from './ui/ToggleGroup'
85
141
  export { SegmentedControl, type SegmentedOption } from './ui/SegmentedControl'
86
142
  export { Breadcrumb, type BreadcrumbItem } from './ui/Breadcrumb'
143
+ export {
144
+ Menubar,
145
+ MenubarCheckboxItem,
146
+ MenubarItem,
147
+ MenubarLabel,
148
+ MenubarMenu,
149
+ MenubarRadioGroup,
150
+ MenubarRadioItem,
151
+ MenubarSeparator,
152
+ MenubarSub,
153
+ type MenubarCheckboxItemProps,
154
+ type MenubarItemProps,
155
+ type MenubarMenuProps,
156
+ type MenubarProps,
157
+ type MenubarRadioGroupProps,
158
+ type MenubarRadioItemProps,
159
+ type MenubarSubProps,
160
+ } from './ui/Menubar'
161
+ export {
162
+ NavigationMenu,
163
+ NavigationMenuContent,
164
+ NavigationMenuItem,
165
+ NavigationMenuLink,
166
+ NavigationMenuPanelLink,
167
+ NavigationMenuTrigger,
168
+ type NavigationMenuContentProps,
169
+ type NavigationMenuLinkProps,
170
+ type NavigationMenuMobileSection,
171
+ type NavigationMenuPanelLinkProps,
172
+ type NavigationMenuProps,
173
+ } from './ui/NavigationMenu'
87
174
  export { AlertDialog } from './ui/AlertDialog'
88
175
  export { ContextMenu, type ContextMenuItem } from './ui/ContextMenu'
89
176
  export { Combobox } from './ui/Combobox'
@@ -92,6 +179,33 @@ export { Stepper, type StepItem, type StepperProps } from './ui/Stepper'
92
179
  export { Timeline, type TimelineItem, type TimelineTone, type TimelineProps } from './ui/Timeline'
93
180
  export { Rating, type RatingProps } from './ui/Rating'
94
181
  export { PricingTable, type PricingTier, type PricingPeriod } from './ui/PricingTable'
182
+ // Page sections — data-driven marketing blocks.
183
+ export type { SectionLink } from './ui/internal/sectionKit'
184
+ export { SiteNav, type SiteNavProps } from './ui/SiteNav'
185
+ export { Hero, type HeroProps, type HeroLayout, type HeroBackground } from './ui/Hero'
186
+ export { FeatureGrid, type FeatureGridProps, type FeatureItem } from './ui/FeatureGrid'
187
+ export {
188
+ Testimonials,
189
+ type TestimonialsProps,
190
+ type TestimonialItem,
191
+ type TestimonialsLayout,
192
+ } from './ui/Testimonials'
193
+ export { Faq, type FaqProps, type FaqItem } from './ui/Faq'
194
+ export { CtaBanner, type CtaBannerProps } from './ui/CtaBanner'
195
+ export {
196
+ ContactSection,
197
+ type ContactSectionProps,
198
+ type ContactField,
199
+ type ContactValues,
200
+ } from './ui/ContactSection'
201
+ export {
202
+ SiteFooter,
203
+ type SiteFooterProps,
204
+ type FooterColumn,
205
+ type SocialLink,
206
+ type SocialNetwork,
207
+ } from './ui/SiteFooter'
208
+ export { TeamGrid, type TeamGridProps, type TeamMember } from './ui/TeamGrid'
95
209
  export { BeforeAfter, type BeforeAfterProps } from './ui/BeforeAfter'
96
210
  export {
97
211
  Configurator,
@@ -124,6 +238,23 @@ export {
124
238
  export { Anchor, type AnchorItem, type AnchorProps } from './ui/Anchor'
125
239
  export { Highlight, type HighlightProps } from './ui/Highlight'
126
240
  export { List, type ListItem } from './ui/List'
241
+ export {
242
+ Item,
243
+ ItemActions,
244
+ ItemContent,
245
+ ItemDescription,
246
+ ItemFooter,
247
+ ItemGroup,
248
+ ItemHeader,
249
+ ItemMedia,
250
+ ItemSeparator,
251
+ ItemTitle,
252
+ type ItemMediaVariant,
253
+ type ItemProps,
254
+ type ItemSize,
255
+ type ItemVariant,
256
+ } from './ui/Item'
257
+ export { ScrollArea, type ScrollAreaOrientation, type ScrollAreaProps } from './ui/ScrollArea'
127
258
  export { Countdown, type CountdownProps } from './ui/Countdown'
128
259
  export { Affix } from './ui/Affix'
129
260
  export { Transfer, type TransferItem, type TransferProps } from './ui/Transfer'
@@ -151,7 +282,31 @@ export { Clock, type ClockProps } from './ui/Clock'
151
282
 
152
283
  // Conversation / AI, transitions, and depth surfaces (roadmap phase 1).
153
284
  export { ChatThread, type ChatThreadProps } from './ui/ChatThread'
154
- export { Message, type MessageProps, type ChatRole } from './ui/Message'
285
+ export {
286
+ Message,
287
+ MessageGroup,
288
+ type MessageProps,
289
+ type MessageGroupProps,
290
+ type MessageVariant,
291
+ type MessageAlign,
292
+ type ChatRole,
293
+ } from './ui/Message'
294
+ export { type ChatThreadApi } from './ui/ChatThread'
295
+ export { ChatMarker, type ChatMarkerProps, type ChatMarkerVariant } from './ui/ChatMarker'
296
+ export {
297
+ Attachment,
298
+ AttachmentMedia,
299
+ AttachmentContent,
300
+ AttachmentTitle,
301
+ AttachmentDescription,
302
+ AttachmentActions,
303
+ AttachmentAction,
304
+ AttachmentGroup,
305
+ type AttachmentProps,
306
+ type AttachmentActionProps,
307
+ type AttachmentState,
308
+ type AttachmentOrientation,
309
+ } from './ui/Attachment'
155
310
  export { StreamingText, type StreamingTextProps } from './ui/StreamingText'
156
311
  export { Citation, type CitationProps, SourceList, type SourceListProps } from './ui/Citation'
157
312
  export { PromptComposer, type PromptComposerProps } from './ui/PromptComposer'
@@ -330,8 +485,42 @@ export { flyToCart, type FlyToCartOptions } from './lib/flyToCart'
330
485
  // Layout (src/layout) — generic shell + backdrop + toggles. App-coupled pieces
331
486
  // (Sidebar, Topbar, CompanySwitcher, DemoBanner, CommandPalette) stay in the app.
332
487
  export { AppShell } from './layout/AppShell'
488
+ export {
489
+ Sidebar,
490
+ SidebarProvider,
491
+ SidebarTrigger,
492
+ SidebarRail,
493
+ SidebarInset,
494
+ SidebarHeader,
495
+ SidebarFooter,
496
+ SidebarContent,
497
+ SidebarGroup,
498
+ SidebarGroupLabel,
499
+ SidebarGroupAction,
500
+ SidebarGroupContent,
501
+ SidebarMenu,
502
+ SidebarMenuItem,
503
+ SidebarMenuButton,
504
+ SidebarMenuAction,
505
+ SidebarMenuBadge,
506
+ SidebarMenuSub,
507
+ SidebarMenuSubItem,
508
+ SidebarMenuSubButton,
509
+ SidebarMenuSkeleton,
510
+ useSidebar,
511
+ type SidebarProps,
512
+ type SidebarProviderProps,
513
+ type SidebarTriggerProps,
514
+ type SidebarGroupActionProps,
515
+ type SidebarMenuButtonProps,
516
+ type SidebarMenuSubProps,
517
+ type SidebarSide,
518
+ type SidebarVariant,
519
+ type SidebarCollapsible,
520
+ } from './ui/Sidebar'
333
521
  export { SpaceBackground } from './layout/SpaceBackground'
334
522
  export { Aurora, type AuroraProps } from './layout/Aurora'
523
+ export type { ModalProps } from './ui/Modal'
335
524
  export { ThemedScenery, type ThemedSceneryProps } from './layout/ThemedScenery'
336
525
  export { SnowScenery } from './layout/SnowScenery'
337
526
  export { ChristmasBackground } from './layout/ChristmasBackground'
@@ -30,9 +30,32 @@ export interface AuroraProps {
30
30
  * @default 'blobs'
31
31
  */
32
32
  variant?: 'blobs' | 'mesh'
33
+ /**
34
+ * Blob palette. Each entry is a CSS color (`'#ff6a3d'`, `'oklch(0.7 0.2 40)'`,
35
+ * `'hsl(var(--brand))'`) or a theme token written as a custom property name
36
+ * (`'--primary'`, `'--accent'`, `'--success'` ...). Entries are assigned to the
37
+ * blobs / mesh points in order and repeat when there are fewer colors than
38
+ * blobs. Defaults to the theme tokens `--primary` / `--accent`, so every theme
39
+ * still tints it for free.
40
+ */
41
+ colors?: string[]
42
+ /**
43
+ * Color of the cursor-following glow, same format as a `colors` entry.
44
+ * @default '--primary'
45
+ */
46
+ glowColor?: string
47
+ /** Peak opacity of the cursor glow, 0..1. @default 0.1 */
48
+ glowOpacity?: number
33
49
  class?: string
34
50
  }
35
51
 
52
+ /** Resolve a palette entry to a CSS color at the given alpha. `--token` => theme token. */
53
+ function colorAt(color: string, alpha: number): string {
54
+ const a = Math.min(1, Math.max(0, alpha))
55
+ if (color.startsWith('--')) return `hsl(var(${color}) / ${a.toFixed(2)})`
56
+ return `color-mix(in srgb, ${color} ${(a * 100).toFixed(0)}%, transparent)`
57
+ }
58
+
36
59
  // Mesh variant — several token-tinted radial gradients composited into one
37
60
  // backdrop (positions chosen to fill the corners without a hard seam).
38
61
  const MESH_STOPS = [
@@ -68,10 +91,16 @@ const BLOBS = [
68
91
  */
69
92
  export function Aurora(props: AuroraProps): JSX.Element {
70
93
  const intensity = (): number => props.intensity ?? 0.45
94
+ // Palette entry for blob/mesh index `i`: custom colors cycle; otherwise the
95
+ // built-in token for that slot.
96
+ const colorFor = (i: number, token: string): string => {
97
+ const list = props.colors
98
+ return list && list.length > 0 ? list[i % list.length] : token
99
+ }
71
100
  const meshBg = (): string =>
72
101
  MESH_STOPS.map(
73
- (s) =>
74
- `radial-gradient(45% 45% at ${s.at}, hsl(var(${s.token}) / ${(intensity() * s.a).toFixed(2)}), transparent 70%)`,
102
+ (s, i) =>
103
+ `radial-gradient(45% 45% at ${s.at}, ${colorAt(colorFor(i, s.token), intensity() * s.a)}, transparent 70%)`,
75
104
  ).join(', ')
76
105
  let root: HTMLDivElement | undefined
77
106
 
@@ -93,11 +122,11 @@ export function Aurora(props: AuroraProps): JSX.Element {
93
122
  when={props.variant === 'mesh'}
94
123
  fallback={
95
124
  <For each={BLOBS}>
96
- {(b) => (
125
+ {(b, i) => (
97
126
  <div
98
127
  class={cn('absolute rounded-full blur-3xl', b.pos, b.size, props.animated && 'aurora-drift')}
99
128
  style={{
100
- background: `radial-gradient(circle, hsl(var(${b.token}) / ${(intensity() * b.a).toFixed(2)}), transparent 70%)`,
129
+ background: `radial-gradient(circle, ${colorAt(colorFor(i(), b.token), intensity() * b.a)}, transparent 70%)`,
101
130
  }}
102
131
  />
103
132
  )}
@@ -110,12 +139,12 @@ export function Aurora(props: AuroraProps): JSX.Element {
110
139
  />
111
140
  </Show>
112
141
  {/* Soft glow that follows the cursor across the backdrop (positioned by
113
- bindPointerFx via left/top on pointermove). */}
142
+ bindPointerFx via transform, coalesced per frame). */}
114
143
  <div
115
144
  id="cursorGlow"
116
- class="absolute h-[520px] w-[520px] rounded-full opacity-0 transition-opacity duration-300"
145
+ class="absolute h-[520px] w-[520px] rounded-full opacity-0 transition-opacity duration-300 will-change-transform"
117
146
  style={{
118
- background: 'radial-gradient(circle, hsl(var(--primary) / 0.1), transparent 70%)',
147
+ background: `radial-gradient(circle, ${colorAt(props.glowColor ?? '--primary', props.glowOpacity ?? 0.1)}, transparent 70%)`,
119
148
  transform: 'translate(-50%,-50%)',
120
149
  }}
121
150
  />
@@ -8,11 +8,29 @@
8
8
  export function bindPointerFx(root: HTMLElement): () => void {
9
9
  const glow = root.querySelector<HTMLElement>('#cursorGlow')
10
10
 
11
+ // The glow is moved with `transform` only (compositor-friendly: no layout, and
12
+ // no repaint of backdrop-filter surfaces in front of it). It is pinned at 0,0
13
+ // once; pointermoves are coalesced to one write per animation frame.
14
+ let frame = 0
15
+ let gx = 0
16
+ let gy = 0
17
+ if (glow) {
18
+ glow.style.left = '0px'
19
+ glow.style.top = '0px'
20
+ glow.style.willChange = 'transform'
21
+ }
22
+ const paintGlow = (): void => {
23
+ frame = 0
24
+ if (!glow) return
25
+ glow.style.transform = `translate3d(${gx}px, ${gy}px, 0) translate(-50%, -50%)`
26
+ }
27
+
11
28
  const onMove = (e: PointerEvent): void => {
12
29
  if (glow) {
13
- glow.style.opacity = '1'
14
- glow.style.left = `${e.clientX}px`
15
- glow.style.top = `${e.clientY}px`
30
+ gx = e.clientX
31
+ gy = e.clientY
32
+ if (glow.style.opacity !== '1') glow.style.opacity = '1'
33
+ if (!frame) frame = requestAnimationFrame(paintGlow)
16
34
  }
17
35
  const el = e.target as Element | null
18
36
  const magnetic = el?.closest?.('.magnetic') as HTMLElement | null
@@ -42,6 +60,7 @@ export function bindPointerFx(root: HTMLElement): () => void {
42
60
  document.addEventListener('pointerout', onOut as EventListener)
43
61
 
44
62
  return () => {
63
+ if (frame) cancelAnimationFrame(frame)
45
64
  document.removeEventListener('pointermove', onMove as EventListener)
46
65
  window.removeEventListener('pointerleave', onLeave)
47
66
  document.removeEventListener('pointerout', onOut as EventListener)
@@ -0,0 +1,57 @@
1
+ // Keeping a portaled popover glued to the trigger it belongs to.
2
+ //
3
+ // DateField, TimeField, DateRangePicker and MultiSelect all render their panel
4
+ // in a <Portal> so it escapes the stacking context of whatever card it sits in.
5
+ // At <body> it cannot be positioned relative to the trigger, so each one places
6
+ // it with `position: fixed` at the trigger's viewport rect — which is correct
7
+ // until the page scrolls and the rect goes stale.
8
+ //
9
+ // All four handled that by CLOSING on any captured scroll, and that made them
10
+ // unusable on a scrolling page: clicking a trigger below the fold scrolls it
11
+ // into view, the scroll event arrives right after the panel opens, and the
12
+ // panel shuts in the same gesture. Reported as a date picker that simply did
13
+ // not respond to clicks; measured in a browser as open-then-closed within
14
+ // 300 ms, with `mousedown` followed by `scroll:document`.
15
+ //
16
+ // A fixed panel does not need to disappear when the page moves — it needs to
17
+ // move with it. This re-places it, and closes only when the trigger has
18
+ // actually left the viewport, where following it would leave a panel hovering
19
+ // over unrelated content.
20
+
21
+ /** Vertical slack, in px, before a trigger counts as gone. Half a row: enough
22
+ * not to close while a sticky header slides over the trigger's top edge. */
23
+ const OFF_SCREEN_SLACK = 16
24
+
25
+ export interface AnchorReflowOptions {
26
+ /** The trigger the panel is anchored to. */
27
+ anchor: () => HTMLElement | undefined
28
+ /** Re-reads the anchor's rect and repositions the panel. */
29
+ place: () => void
30
+ /** Called only when the anchor is no longer on screen. */
31
+ close: () => void
32
+ }
33
+
34
+ /**
35
+ * The scroll/resize handler an anchored popover should register, instead of
36
+ * `close`.
37
+ *
38
+ * Registering it for `scroll` still needs capture (`true`): the scroll may come
39
+ * from any ancestor container, and scroll events do not bubble to `window`.
40
+ */
41
+ export function createAnchorReflow(options: AnchorReflowOptions): () => void {
42
+ return () => {
43
+ const el = options.anchor()
44
+ if (!el) {
45
+ options.close()
46
+ return
47
+ }
48
+ const rect = el.getBoundingClientRect()
49
+ const gone =
50
+ rect.bottom < -OFF_SCREEN_SLACK ||
51
+ rect.top > window.innerHeight + OFF_SCREEN_SLACK ||
52
+ rect.right < 0 ||
53
+ rect.left > window.innerWidth
54
+ if (gone) options.close()
55
+ else options.place()
56
+ }
57
+ }
@@ -0,0 +1,35 @@
1
+ import { splitProps, type JSX, type ParentProps } from 'solid-js'
2
+
3
+ import { cn } from '../lib/cn'
4
+
5
+ interface AspectRatioProps extends ParentProps {
6
+ /** Width divided by height, e.g. `16 / 9`. @default 1 */
7
+ ratio?: number
8
+ class?: string
9
+ }
10
+
11
+ export type { AspectRatioProps }
12
+
13
+ /**
14
+ * Box that keeps a fixed width:height ratio (CSS `aspect-ratio`). Children fill
15
+ * it; give images/video `object-cover`.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * <AspectRatio ratio={16 / 9} class="w-full overflow-hidden rounded-lg">
20
+ * <img src="/cover.jpg" alt="" class="object-cover" />
21
+ * </AspectRatio>
22
+ * ```
23
+ */
24
+ export function AspectRatio(props: AspectRatioProps): JSX.Element {
25
+ const [local, rest] = splitProps(props, ['ratio', 'class', 'children'])
26
+ return (
27
+ <div
28
+ class={cn('relative w-full [&>*]:h-full [&>*]:w-full', local.class)}
29
+ style={{ 'aspect-ratio': String(local.ratio ?? 1) }}
30
+ {...rest}
31
+ >
32
+ {local.children}
33
+ </div>
34
+ )
35
+ }
@@ -0,0 +1,189 @@
1
+ // Attachment — a file/image chip for chat UIs, built on the `Item` row primitive.
2
+ // Adds an upload lifecycle (idle / uploading / processing / error / done), a
3
+ // progress bar and a horizontally scrolling group.
4
+ import { splitProps, type JSX, type ParentProps, Show } from 'solid-js'
5
+
6
+ import { cn } from '../lib/cn'
7
+ import { Item, ItemActions, ItemContent, ItemDescription, ItemMedia, ItemTitle, type ItemSize } from './Item'
8
+ import { ScrollArea } from './ScrollArea'
9
+
10
+ export type AttachmentState = 'idle' | 'uploading' | 'processing' | 'error' | 'done'
11
+ export type AttachmentOrientation = 'horizontal' | 'vertical'
12
+
13
+ interface AttachmentProps extends ParentProps {
14
+ /** Lifecycle state. `uploading`/`processing` set `aria-busy`; `error` uses destructive tokens. @default 'idle' */
15
+ state?: AttachmentState
16
+ /** Density, forwarded to `Item`. @default 'default' */
17
+ size?: ItemSize
18
+ /** Lay media and text side by side, or stacked. @default 'horizontal' */
19
+ orientation?: AttachmentOrientation
20
+ /**
21
+ * Upload progress, 0–100. Only used while `state="uploading"`; when omitted
22
+ * the bar is indeterminate (no `aria-valuenow`).
23
+ */
24
+ progress?: number
25
+ /** Accessible name of the progress bar. @default 'Uploading' / 'Processing' */
26
+ progressLabel?: string
27
+ class?: string
28
+ }
29
+
30
+ export type { AttachmentProps }
31
+
32
+ const clamp = (n: number): number => Math.min(100, Math.max(0, n))
33
+
34
+ /**
35
+ * A file or image attachment: `AttachmentMedia`, an `AttachmentContent` with
36
+ * title/description and trailing `AttachmentActions`. Group several in an
37
+ * `AttachmentGroup` (horizontal scroll-snap row).
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * <Attachment state="uploading" progress={42}>
42
+ * <AttachmentMedia><FileText /></AttachmentMedia>
43
+ * <AttachmentContent>
44
+ * <AttachmentTitle>roadmap.pdf</AttachmentTitle>
45
+ * <AttachmentDescription>42% of 2.4 MB</AttachmentDescription>
46
+ * </AttachmentContent>
47
+ * <AttachmentActions>
48
+ * <AttachmentAction label="Cancel upload"><X /></AttachmentAction>
49
+ * </AttachmentActions>
50
+ * </Attachment>
51
+ * ```
52
+ */
53
+ export function Attachment(props: AttachmentProps): JSX.Element {
54
+ const [local] = splitProps(props, [
55
+ 'state',
56
+ 'size',
57
+ 'orientation',
58
+ 'progress',
59
+ 'progressLabel',
60
+ 'class',
61
+ 'children',
62
+ ])
63
+ const state = (): AttachmentState => local.state ?? 'idle'
64
+ const busy = (): boolean => state() === 'uploading' || state() === 'processing'
65
+ const determinate = (): boolean => state() === 'uploading' && local.progress !== undefined
66
+ return (
67
+ <div
68
+ data-slot="attachment"
69
+ data-state={state()}
70
+ data-orientation={local.orientation ?? 'horizontal'}
71
+ aria-busy={busy() ? 'true' : undefined}
72
+ class={cn(
73
+ 'relative w-full overflow-hidden rounded-lg border bg-card/60 text-card-foreground',
74
+ state() === 'error'
75
+ ? 'border-destructive/50 bg-destructive/10 text-destructive [&_[data-slot=item-description]]:text-destructive/80'
76
+ : 'border-border',
77
+ local.class,
78
+ )}
79
+ >
80
+ <Item
81
+ size={local.size}
82
+ class={cn(
83
+ 'flex-nowrap border-0',
84
+ local.orientation === 'vertical' && 'flex-col items-start',
85
+ state() === 'processing' && 'animate-pulse',
86
+ )}
87
+ >
88
+ {local.children}
89
+ </Item>
90
+ <Show when={busy()}>
91
+ <div
92
+ role="progressbar"
93
+ aria-label={local.progressLabel ?? (state() === 'uploading' ? 'Uploading' : 'Processing')}
94
+ aria-valuemin={0}
95
+ aria-valuemax={100}
96
+ aria-valuenow={determinate() ? Math.round(clamp(local.progress ?? 0)) : undefined}
97
+ class="absolute inset-x-0 bottom-0 h-1 bg-muted"
98
+ >
99
+ <div
100
+ class={cn(
101
+ 'h-full bg-primary transition-[width] duration-200',
102
+ !determinate() && 'w-1/3 animate-pulse',
103
+ )}
104
+ style={determinate() ? { width: `${clamp(local.progress ?? 0)}%` } : undefined}
105
+ />
106
+ </div>
107
+ </Show>
108
+ </div>
109
+ )
110
+ }
111
+
112
+ interface SlotProps extends ParentProps {
113
+ class?: string
114
+ }
115
+
116
+ /** Leading visual: an icon tile (`icon`, default) or an image thumbnail (`image`). */
117
+ export function AttachmentMedia(props: SlotProps & { variant?: 'icon' | 'image' }): JSX.Element {
118
+ return (
119
+ <ItemMedia variant={props.variant ?? 'icon'} class={props.class}>
120
+ {props.children}
121
+ </ItemMedia>
122
+ )
123
+ }
124
+
125
+ /** The text column. */
126
+ export function AttachmentContent(props: SlotProps): JSX.Element {
127
+ return <ItemContent class={props.class}>{props.children}</ItemContent>
128
+ }
129
+
130
+ /** File name; truncates on one line. */
131
+ export function AttachmentTitle(props: SlotProps): JSX.Element {
132
+ return <ItemTitle class={cn('block truncate', props.class)}>{props.children}</ItemTitle>
133
+ }
134
+
135
+ /** Secondary line: size, status or error message. */
136
+ export function AttachmentDescription(props: SlotProps): JSX.Element {
137
+ return <ItemDescription class={cn('line-clamp-1 text-xs', props.class)}>{props.children}</ItemDescription>
138
+ }
139
+
140
+ /** Trailing controls. */
141
+ export function AttachmentActions(props: SlotProps): JSX.Element {
142
+ return <ItemActions class={cn('gap-1', props.class)}>{props.children}</ItemActions>
143
+ }
144
+
145
+ interface AttachmentActionProps extends ParentProps {
146
+ /** Accessible name (required: the button only holds an icon). */
147
+ label: string
148
+ onClick?: (event: MouseEvent) => void
149
+ disabled?: boolean
150
+ class?: string
151
+ }
152
+
153
+ export type { AttachmentActionProps }
154
+
155
+ /** Icon button inside `AttachmentActions`. */
156
+ export function AttachmentAction(props: AttachmentActionProps): JSX.Element {
157
+ return (
158
+ <button
159
+ type="button"
160
+ aria-label={props.label}
161
+ disabled={props.disabled}
162
+ onClick={(e) => props.onClick?.(e)}
163
+ class={cn(
164
+ 'grid h-7 w-7 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&>svg]:h-4 [&>svg]:w-4',
165
+ props.class,
166
+ )}
167
+ >
168
+ {props.children}
169
+ </button>
170
+ )
171
+ }
172
+
173
+ /** Horizontal scroll-snap row of attachments. */
174
+ export function AttachmentGroup(props: SlotProps & { label?: string }): JSX.Element {
175
+ return (
176
+ <ScrollArea
177
+ orientation="horizontal"
178
+ label={props.label ?? 'Attachments'}
179
+ class={cn('w-full snap-x snap-mandatory rounded-lg', props.class)}
180
+ >
181
+ <div
182
+ data-slot="attachment-group"
183
+ class="flex w-max gap-2 p-1 [&>[data-slot=attachment]]:w-64 [&>[data-slot=attachment]]:shrink-0 [&>[data-slot=attachment]]:snap-start"
184
+ >
185
+ {props.children}
186
+ </div>
187
+ </ScrollArea>
188
+ )
189
+ }