@discourser/design-system 0.3.1 → 0.5.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 (36) hide show
  1. package/README.md +12 -4
  2. package/dist/styles.css +5126 -0
  3. package/guidelines/Guidelines.md +92 -41
  4. package/guidelines/components/accordion.md +732 -0
  5. package/guidelines/components/avatar.md +1015 -0
  6. package/guidelines/components/badge.md +728 -0
  7. package/guidelines/components/button.md +75 -40
  8. package/guidelines/components/card.md +84 -25
  9. package/guidelines/components/checkbox.md +671 -0
  10. package/guidelines/components/dialog.md +619 -31
  11. package/guidelines/components/drawer.md +1616 -0
  12. package/guidelines/components/heading.md +576 -0
  13. package/guidelines/components/icon-button.md +92 -37
  14. package/guidelines/components/input-addon.md +685 -0
  15. package/guidelines/components/input-group.md +830 -0
  16. package/guidelines/components/input.md +92 -37
  17. package/guidelines/components/popover.md +1271 -0
  18. package/guidelines/components/progress.md +836 -0
  19. package/guidelines/components/radio-group.md +852 -0
  20. package/guidelines/components/select.md +1662 -0
  21. package/guidelines/components/skeleton.md +802 -0
  22. package/guidelines/components/slider.md +911 -0
  23. package/guidelines/components/spinner.md +783 -0
  24. package/guidelines/components/switch.md +105 -38
  25. package/guidelines/components/tabs.md +1488 -0
  26. package/guidelines/components/textarea.md +495 -0
  27. package/guidelines/components/toast.md +784 -0
  28. package/guidelines/components/tooltip.md +912 -0
  29. package/guidelines/design-tokens/colors.md +309 -72
  30. package/guidelines/design-tokens/elevation.md +615 -45
  31. package/guidelines/design-tokens/spacing.md +654 -74
  32. package/guidelines/design-tokens/typography.md +432 -50
  33. package/guidelines/overview-components.md +60 -8
  34. package/guidelines/overview-imports.md +314 -0
  35. package/guidelines/overview-patterns.md +3852 -0
  36. package/package.json +4 -2
@@ -0,0 +1,1271 @@
1
+ # Popover
2
+
3
+ **Purpose:** A floating panel that appears near a trigger element to display contextual content, menus, forms, or additional information. Built on Ark UI's Popover primitive with intelligent positioning and smooth animations.
4
+
5
+ ## When to Use This Component
6
+
7
+ Use Popover when you need to **display rich contextual content or interactive elements near a trigger** without navigating away from the current page.
8
+
9
+ ### Decision Tree
10
+
11
+ | Scenario | Use Popover? | Alternative | Reasoning |
12
+ | ------------------------------------------ | ------------ | ----------- | -------------------------------------------------- |
13
+ | Contextual menus with actions | ✅ Yes | - | Popover shows rich content near the trigger |
14
+ | Forms or color pickers attached to element | ✅ Yes | - | Perfect for inline editing without page navigation |
15
+ | User profile preview on hover/click | ✅ Yes | - | Shows detailed info without full page load |
16
+ | Simple text hints or labels | ❌ No | Tooltip | Tooltip is lighter and better for brief help text |
17
+ | Critical actions requiring focus | ❌ No | Dialog | Dialog centers attention and is modal |
18
+ | Navigation menus (mobile) | ❌ No | Drawer | Drawer slides from edge, better for mobile menus |
19
+
20
+ ### Component Comparison
21
+
22
+ ```typescript
23
+ // ✅ Popover - Rich contextual menu with form
24
+ <Popover.Root>
25
+ <Popover.Trigger asChild>
26
+ <Button>Add Comment</Button>
27
+ </Popover.Trigger>
28
+ <Popover.Positioner>
29
+ <Popover.Content>
30
+ <Popover.Title>New Comment</Popover.Title>
31
+ <Popover.Description>Add your thoughts below</Popover.Description>
32
+ <Textarea placeholder="Type your comment..." />
33
+ <Button>Submit</Button>
34
+ </Popover.Content>
35
+ </Popover.Positioner>
36
+ </Popover.Root>
37
+
38
+ // ❌ Don't use Popover for simple hints - Use Tooltip
39
+ <Popover.Root>
40
+ <Popover.Trigger asChild>
41
+ <IconButton><InfoIcon /></IconButton>
42
+ </Popover.Trigger>
43
+ <Popover.Positioner>
44
+ <Popover.Content>
45
+ This is a simple hint
46
+ </Popover.Content>
47
+ </Popover.Positioner>
48
+ </Popover.Root>
49
+
50
+ // ✅ Better: Use Tooltip for brief hints
51
+ <Tooltip content="This is a simple hint">
52
+ <IconButton><InfoIcon /></IconButton>
53
+ </Tooltip>
54
+
55
+ // ❌ Don't use Popover for critical modals - Use Dialog
56
+ <Popover.Root>
57
+ <Popover.Content>
58
+ <Popover.Title>Delete Account?</Popover.Title>
59
+ <Button>Confirm Delete</Button>
60
+ </Popover.Content>
61
+ </Popover.Root>
62
+
63
+ // ✅ Better: Use Dialog for important confirmations
64
+ <Dialog.Root>
65
+ <Dialog.Content>
66
+ <Dialog.Title>Delete Account?</Dialog.Title>
67
+ <Dialog.Description>This cannot be undone.</Dialog.Description>
68
+ <Dialog.Footer>
69
+ <Button variant="outlined">Cancel</Button>
70
+ <Button colorPalette="error">Delete</Button>
71
+ </Dialog.Footer>
72
+ </Dialog.Content>
73
+ </Dialog.Root>
74
+ ```
75
+
76
+ ## Import
77
+
78
+ ```typescript
79
+ import { Popover } from '@discourser/design-system';
80
+ ```
81
+
82
+ ## Component Structure
83
+
84
+ Popover is a compound component built using Ark UI's Popover primitive. It consists of several parts that work together:
85
+
86
+ ### Core Components
87
+
88
+ | Component | Purpose | Required |
89
+ | ---------------------- | --------------------------------------- | ----------- |
90
+ | `Popover.Root` | Main container and state manager | Yes |
91
+ | `Popover.Trigger` | Element that opens the popover | Yes |
92
+ | `Popover.Positioner` | Positions popover relative to trigger | Yes |
93
+ | `Popover.Content` | Main popover panel | Yes |
94
+ | `Popover.Title` | Popover heading (for accessibility) | Recommended |
95
+ | `Popover.Description` | Popover description (for accessibility) | Optional |
96
+ | `Popover.CloseTrigger` | Button to close popover | Optional |
97
+
98
+ ### Layout Components
99
+
100
+ | Component | Purpose | Usage |
101
+ | ---------------- | -------------------------------------- | ----------- |
102
+ | `Popover.Header` | Top section for title and close button | Optional |
103
+ | `Popover.Body` | Main scrollable content area | Recommended |
104
+ | `Popover.Footer` | Bottom section for actions | Optional |
105
+
106
+ ### Positioning Components
107
+
108
+ | Component | Purpose | Usage |
109
+ | ------------------- | --------------------------------------------- | ------------------------ |
110
+ | `Popover.Anchor` | Alternative anchor point (instead of trigger) | Optional |
111
+ | `Popover.Arrow` | Visual arrow pointing to trigger | Optional |
112
+ | `Popover.ArrowTip` | Arrow border styling | Auto-included with Arrow |
113
+ | `Popover.Indicator` | Visual indicator on trigger | Optional |
114
+
115
+ ### Context
116
+
117
+ | Component | Purpose |
118
+ | ----------------- | ------------------------------------- |
119
+ | `Popover.Context` | Access popover state programmatically |
120
+
121
+ ## Variants
122
+
123
+ Popover uses intelligent positioning based on available space. Unlike Drawer, it doesn't have visual variants but relies on positioning logic.
124
+
125
+ ### Default Behavior
126
+
127
+ - **Width**: `sm` (384px/24rem) by default
128
+ - **Positioning**: Auto-adjusts based on viewport space
129
+ - **Animation**: Scale + fade (fast duration)
130
+ - **Arrow**: Optional, points to trigger element
131
+ - **Max Height**: Constrained by available viewport height
132
+
133
+ ## Props
134
+
135
+ ### Root Props
136
+
137
+ | Prop | Type | Default | Description |
138
+ | ------------------------ | -------------------------------------- | -------------- | --------------------------------------------- |
139
+ | `open` | `boolean` | - | Controlled open state |
140
+ | `defaultOpen` | `boolean` | `false` | Initial open state (uncontrolled) |
141
+ | `onOpenChange` | `(details: { open: boolean }) => void` | - | Callback when open state changes |
142
+ | `closeOnInteractOutside` | `boolean` | `true` | Close when clicking outside |
143
+ | `closeOnEscapeKeyDown` | `boolean` | `true` | Close when pressing Escape |
144
+ | `autoFocus` | `boolean` | `true` | Auto-focus first element when opened |
145
+ | `modal` | `boolean` | `false` | Whether popover is modal (blocks interaction) |
146
+ | `portalled` | `boolean` | `true` | Render in portal (outside DOM hierarchy) |
147
+ | `positioning` | `PositioningOptions` | - | Custom positioning configuration |
148
+ | `unmountOnExit` | `boolean` | `true` | Remove from DOM when closed |
149
+ | `lazyMount` | `boolean` | `true` | Mount content only when first opened |
150
+ | `id` | `string` | auto-generated | Unique ID for accessibility |
151
+
152
+ ### Positioning Options
153
+
154
+ ```typescript
155
+ interface PositioningOptions {
156
+ placement?:
157
+ | 'top'
158
+ | 'top-start'
159
+ | 'top-end'
160
+ | 'bottom'
161
+ | 'bottom-start'
162
+ | 'bottom-end'
163
+ | 'left'
164
+ | 'left-start'
165
+ | 'left-end'
166
+ | 'right'
167
+ | 'right-start'
168
+ | 'right-end';
169
+ offset?: { mainAxis?: number; crossAxis?: number };
170
+ gutter?: number;
171
+ flip?: boolean;
172
+ slide?: boolean;
173
+ overlap?: boolean;
174
+ sameWidth?: boolean;
175
+ fitViewport?: boolean;
176
+ }
177
+ ```
178
+
179
+ ### Content Props
180
+
181
+ All compound components accept standard HTML attributes plus styling props from Panda CSS.
182
+
183
+ ## Examples
184
+
185
+ ### Basic Usage
186
+
187
+ ```typescript
188
+ import { Popover, Button } from '@discourser/design-system';
189
+
190
+ function BasicPopover() {
191
+ return (
192
+ <Popover.Root>
193
+ <Popover.Trigger asChild>
194
+ <Button variant="outlined">Open Popover</Button>
195
+ </Popover.Trigger>
196
+
197
+ <Popover.Positioner>
198
+ <Popover.Content>
199
+ <Popover.Arrow>
200
+ <Popover.ArrowTip />
201
+ </Popover.Arrow>
202
+
203
+ <Popover.Header>
204
+ <Popover.Title>Popover Title</Popover.Title>
205
+ </Popover.Header>
206
+
207
+ <Popover.Body>
208
+ <p>This is the popover content. It can contain any elements.</p>
209
+ </Popover.Body>
210
+ </Popover.Content>
211
+ </Popover.Positioner>
212
+ </Popover.Root>
213
+ );
214
+ }
215
+ ```
216
+
217
+ ### With Close Button
218
+
219
+ ```typescript
220
+ import { Popover, Button, IconButton } from '@discourser/design-system';
221
+ import { XIcon } from 'your-icon-library';
222
+
223
+ function PopoverWithClose() {
224
+ return (
225
+ <Popover.Root>
226
+ <Popover.Trigger asChild>
227
+ <Button>Info</Button>
228
+ </Popover.Trigger>
229
+
230
+ <Popover.Positioner>
231
+ <Popover.Content>
232
+ <Popover.Arrow />
233
+
234
+ <Popover.Header>
235
+ <Popover.Title>Information</Popover.Title>
236
+ <Popover.Description>
237
+ Additional details about this feature
238
+ </Popover.Description>
239
+ <Popover.CloseTrigger asChild>
240
+ <IconButton aria-label="Close" variant="text" size="sm">
241
+ <XIcon />
242
+ </IconButton>
243
+ </Popover.CloseTrigger>
244
+ </Popover.Header>
245
+
246
+ <Popover.Body>
247
+ <p>Detailed explanation goes here...</p>
248
+ </Popover.Body>
249
+
250
+ <Popover.Footer>
251
+ <Button size="sm" variant="text">Learn More</Button>
252
+ </Popover.Footer>
253
+ </Popover.Content>
254
+ </Popover.Positioner>
255
+ </Popover.Root>
256
+ );
257
+ }
258
+ ```
259
+
260
+ ### Controlled Popover
261
+
262
+ ```typescript
263
+ import { useState } from 'react';
264
+ import { Popover, Button } from '@discourser/design-system';
265
+
266
+ function ControlledPopover() {
267
+ const [open, setOpen] = useState(false);
268
+
269
+ const handleConfirm = () => {
270
+ console.log('Confirmed!');
271
+ setOpen(false);
272
+ };
273
+
274
+ return (
275
+ <>
276
+ <Button onClick={() => setOpen(true)}>Show Options</Button>
277
+
278
+ <Popover.Root open={open} onOpenChange={(details) => setOpen(details.open)}>
279
+ <Popover.Positioner>
280
+ <Popover.Content>
281
+ <Popover.Arrow />
282
+
283
+ <Popover.Header>
284
+ <Popover.Title>Choose Action</Popover.Title>
285
+ </Popover.Header>
286
+
287
+ <Popover.Body>
288
+ <p>Select an option below:</p>
289
+ </Popover.Body>
290
+
291
+ <Popover.Footer>
292
+ <Button variant="outlined" size="sm" onClick={() => setOpen(false)}>
293
+ Cancel
294
+ </Button>
295
+ <Button size="sm" onClick={handleConfirm}>
296
+ Confirm
297
+ </Button>
298
+ </Popover.Footer>
299
+ </Popover.Content>
300
+ </Popover.Positioner>
301
+ </Popover.Root>
302
+ </>
303
+ );
304
+ }
305
+ ```
306
+
307
+ ### Custom Positioning
308
+
309
+ ```typescript
310
+ // Position at top
311
+ <Popover.Root positioning={{ placement: 'top' }}>
312
+ <Popover.Trigger asChild>
313
+ <Button>Top Popover</Button>
314
+ </Popover.Trigger>
315
+ <Popover.Positioner>
316
+ <Popover.Content>
317
+ <Popover.Arrow />
318
+ <Popover.Body>Content appears above trigger</Popover.Body>
319
+ </Popover.Content>
320
+ </Popover.Positioner>
321
+ </Popover.Root>
322
+
323
+ // Position at bottom-start (left-aligned)
324
+ <Popover.Root positioning={{ placement: 'bottom-start' }}>
325
+ <Popover.Trigger asChild>
326
+ <Button>Menu</Button>
327
+ </Popover.Trigger>
328
+ <Popover.Positioner>
329
+ <Popover.Content>
330
+ <Popover.Body>Aligned to left edge of trigger</Popover.Body>
331
+ </Popover.Content>
332
+ </Popover.Positioner>
333
+ </Popover.Root>
334
+
335
+ // Custom offset and gutter
336
+ <Popover.Root
337
+ positioning={{
338
+ placement: 'right',
339
+ gutter: 16, // Space from trigger
340
+ offset: { mainAxis: 10, crossAxis: 0 },
341
+ }}
342
+ >
343
+ <Popover.Trigger asChild>
344
+ <Button>Right Popover</Button>
345
+ </Popover.Trigger>
346
+ <Popover.Positioner>
347
+ <Popover.Content>
348
+ <Popover.Body>Custom spacing from trigger</Popover.Body>
349
+ </Popover.Content>
350
+ </Popover.Positioner>
351
+ </Popover.Root>
352
+
353
+ // Same width as trigger
354
+ <Popover.Root positioning={{ sameWidth: true }}>
355
+ <Popover.Trigger asChild>
356
+ <Button>Select Option</Button>
357
+ </Popover.Trigger>
358
+ <Popover.Positioner>
359
+ <Popover.Content>
360
+ <Popover.Body>Width matches trigger button</Popover.Body>
361
+ </Popover.Content>
362
+ </Popover.Positioner>
363
+ </Popover.Root>
364
+ ```
365
+
366
+ ### Form in Popover
367
+
368
+ ```typescript
369
+ import { Popover, Button, Input } from '@discourser/design-system';
370
+ import { useState } from 'react';
371
+
372
+ function FormPopover() {
373
+ const [email, setEmail] = useState('');
374
+ const [submitted, setSubmitted] = useState(false);
375
+
376
+ const handleSubmit = (e: React.FormEvent) => {
377
+ e.preventDefault();
378
+ console.log('Email:', email);
379
+ setSubmitted(true);
380
+ };
381
+
382
+ return (
383
+ <Popover.Root>
384
+ <Popover.Trigger asChild>
385
+ <Button>Subscribe</Button>
386
+ </Popover.Trigger>
387
+
388
+ <Popover.Positioner>
389
+ <Popover.Content>
390
+ <Popover.Arrow />
391
+
392
+ {!submitted ? (
393
+ <form onSubmit={handleSubmit}>
394
+ <Popover.Header>
395
+ <Popover.Title>Newsletter Signup</Popover.Title>
396
+ <Popover.Description>
397
+ Get weekly updates delivered to your inbox
398
+ </Popover.Description>
399
+ </Popover.Header>
400
+
401
+ <Popover.Body>
402
+ <Input
403
+ type="email"
404
+ label="Email Address"
405
+ value={email}
406
+ onChange={(e) => setEmail(e.target.value)}
407
+ required
408
+ />
409
+ </Popover.Body>
410
+
411
+ <Popover.Footer>
412
+ <Popover.CloseTrigger asChild>
413
+ <Button variant="outlined" size="sm" type="button">
414
+ Cancel
415
+ </Button>
416
+ </Popover.CloseTrigger>
417
+ <Button size="sm" type="submit">
418
+ Subscribe
419
+ </Button>
420
+ </Popover.Footer>
421
+ </form>
422
+ ) : (
423
+ <Popover.Body>
424
+ <p>Thank you for subscribing!</p>
425
+ </Popover.Body>
426
+ )}
427
+ </Popover.Content>
428
+ </Popover.Positioner>
429
+ </Popover.Root>
430
+ );
431
+ }
432
+ ```
433
+
434
+ ### Action Menu Popover
435
+
436
+ ```typescript
437
+ import { Popover, Button } from '@discourser/design-system';
438
+ import { MoreVerticalIcon, EditIcon, DeleteIcon, ShareIcon } from 'your-icon-library';
439
+
440
+ function ActionMenuPopover() {
441
+ const actions = [
442
+ { icon: <EditIcon />, label: 'Edit', onClick: () => console.log('Edit') },
443
+ { icon: <ShareIcon />, label: 'Share', onClick: () => console.log('Share') },
444
+ { icon: <DeleteIcon />, label: 'Delete', onClick: () => console.log('Delete') },
445
+ ];
446
+
447
+ return (
448
+ <Popover.Root>
449
+ <Popover.Trigger asChild>
450
+ <Button variant="text" size="sm">
451
+ <MoreVerticalIcon />
452
+ </Button>
453
+ </Popover.Trigger>
454
+
455
+ <Popover.Positioner>
456
+ <Popover.Content>
457
+ <Popover.Body
458
+ className={css({
459
+ display: 'flex',
460
+ flexDirection: 'column',
461
+ gap: '1',
462
+ p: '2',
463
+ })}
464
+ >
465
+ {actions.map((action) => (
466
+ <Popover.CloseTrigger key={action.label} asChild>
467
+ <button
468
+ onClick={action.onClick}
469
+ className={css({
470
+ display: 'flex',
471
+ alignItems: 'center',
472
+ gap: '2',
473
+ p: '2',
474
+ borderRadius: 'md',
475
+ cursor: 'pointer',
476
+ bg: 'transparent',
477
+ color: 'fg.default',
478
+ textAlign: 'left',
479
+ width: '100%',
480
+ border: 'none',
481
+ _hover: { bg: 'gray.a3' },
482
+ })}
483
+ >
484
+ {action.icon}
485
+ <span>{action.label}</span>
486
+ </button>
487
+ </Popover.CloseTrigger>
488
+ ))}
489
+ </Popover.Body>
490
+ </Popover.Content>
491
+ </Popover.Positioner>
492
+ </Popover.Root>
493
+ );
494
+ }
495
+ ```
496
+
497
+ ### User Profile Popover
498
+
499
+ ```typescript
500
+ import { Popover, Button, Avatar } from '@discourser/design-system';
501
+
502
+ function UserProfilePopover({ user }) {
503
+ return (
504
+ <Popover.Root>
505
+ <Popover.Trigger asChild>
506
+ <button className={css({ cursor: 'pointer', border: 'none', bg: 'transparent' })}>
507
+ <Avatar src={user.avatar} name={user.name} />
508
+ </button>
509
+ </Popover.Trigger>
510
+
511
+ <Popover.Positioner>
512
+ <Popover.Content>
513
+ <Popover.Arrow />
514
+
515
+ <Popover.Header>
516
+ <div className={css({ display: 'flex', alignItems: 'center', gap: '3' })}>
517
+ <Avatar src={user.avatar} name={user.name} size="lg" />
518
+ <div>
519
+ <Popover.Title>{user.name}</Popover.Title>
520
+ <Popover.Description>{user.email}</Popover.Description>
521
+ </div>
522
+ </div>
523
+ </Popover.Header>
524
+
525
+ <Popover.Body>
526
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: '2' })}>
527
+ <a href="/profile">View Profile</a>
528
+ <a href="/settings">Settings</a>
529
+ <a href="/help">Help & Support</a>
530
+ </div>
531
+ </Popover.Body>
532
+
533
+ <Popover.Footer>
534
+ <Button size="sm" variant="outlined" fullWidth>
535
+ Sign Out
536
+ </Button>
537
+ </Popover.Footer>
538
+ </Popover.Content>
539
+ </Popover.Positioner>
540
+ </Popover.Root>
541
+ );
542
+ }
543
+ ```
544
+
545
+ ### Using Anchor (Separate Trigger and Anchor)
546
+
547
+ ```typescript
548
+ import { Popover, Button } from '@discourser/design-system';
549
+ import { useRef } from 'react';
550
+
551
+ function AnchorPopover() {
552
+ const anchorRef = useRef<HTMLDivElement>(null);
553
+
554
+ return (
555
+ <div>
556
+ <div ref={anchorRef} className={css({ p: '4', bg: 'gray.a3', borderRadius: 'md' })}>
557
+ This div is the anchor point
558
+ </div>
559
+
560
+ <Popover.Root>
561
+ <Popover.Anchor ref={anchorRef} />
562
+
563
+ <Popover.Trigger asChild>
564
+ <Button>Show Popover</Button>
565
+ </Popover.Trigger>
566
+
567
+ <Popover.Positioner>
568
+ <Popover.Content>
569
+ <Popover.Arrow />
570
+ <Popover.Body>
571
+ Popover appears near the anchor div, not the button
572
+ </Popover.Body>
573
+ </Popover.Content>
574
+ </Popover.Positioner>
575
+ </Popover.Root>
576
+ </div>
577
+ );
578
+ }
579
+ ```
580
+
581
+ ### Using Context
582
+
583
+ ```typescript
584
+ import { Popover, Button } from '@discourser/design-system';
585
+
586
+ function PopoverWithContext() {
587
+ return (
588
+ <Popover.Root>
589
+ <Popover.Trigger asChild>
590
+ <Button>Open</Button>
591
+ </Popover.Trigger>
592
+
593
+ <Popover.Positioner>
594
+ <Popover.Content>
595
+ <Popover.Arrow />
596
+
597
+ <Popover.Body>
598
+ <Popover.Context>
599
+ {(context) => (
600
+ <div>
601
+ <p>Popover is {context.open ? 'open' : 'closed'}</p>
602
+ <Button size="sm" onClick={() => context.setOpen(false)}>
603
+ Close Programmatically
604
+ </Button>
605
+ </div>
606
+ )}
607
+ </Popover.Context>
608
+ </Popover.Body>
609
+ </Popover.Content>
610
+ </Popover.Positioner>
611
+ </Popover.Root>
612
+ );
613
+ }
614
+ ```
615
+
616
+ ### Hover Trigger Popover
617
+
618
+ ```typescript
619
+ function HoverPopover() {
620
+ const [open, setOpen] = useState(false);
621
+ let timeoutId: NodeJS.Timeout;
622
+
623
+ const handleMouseEnter = () => {
624
+ clearTimeout(timeoutId);
625
+ setOpen(true);
626
+ };
627
+
628
+ const handleMouseLeave = () => {
629
+ timeoutId = setTimeout(() => setOpen(false), 300);
630
+ };
631
+
632
+ return (
633
+ <Popover.Root open={open} onOpenChange={(e) => setOpen(e.open)}>
634
+ <Popover.Trigger
635
+ asChild
636
+ onMouseEnter={handleMouseEnter}
637
+ onMouseLeave={handleMouseLeave}
638
+ >
639
+ <Button variant="text">Hover Me</Button>
640
+ </Popover.Trigger>
641
+
642
+ <Popover.Positioner>
643
+ <Popover.Content
644
+ onMouseEnter={handleMouseEnter}
645
+ onMouseLeave={handleMouseLeave}
646
+ >
647
+ <Popover.Arrow />
648
+ <Popover.Body>
649
+ <p>This popover appears on hover</p>
650
+ </Popover.Body>
651
+ </Popover.Content>
652
+ </Popover.Positioner>
653
+ </Popover.Root>
654
+ );
655
+ }
656
+ ```
657
+
658
+ ## Common Patterns
659
+
660
+ ### Confirmation Popover
661
+
662
+ ```typescript
663
+ function ConfirmationPopover({ onConfirm, children }) {
664
+ const [open, setOpen] = useState(false);
665
+
666
+ const handleConfirm = () => {
667
+ onConfirm();
668
+ setOpen(false);
669
+ };
670
+
671
+ return (
672
+ <Popover.Root open={open} onOpenChange={(e) => setOpen(e.open)}>
673
+ <Popover.Trigger asChild>
674
+ {children}
675
+ </Popover.Trigger>
676
+
677
+ <Popover.Positioner>
678
+ <Popover.Content>
679
+ <Popover.Arrow />
680
+
681
+ <Popover.Header>
682
+ <Popover.Title>Are you sure?</Popover.Title>
683
+ <Popover.Description>
684
+ This action cannot be undone
685
+ </Popover.Description>
686
+ </Popover.Header>
687
+
688
+ <Popover.Footer>
689
+ <Button variant="outlined" size="sm" onClick={() => setOpen(false)}>
690
+ Cancel
691
+ </Button>
692
+ <Button size="sm" onClick={handleConfirm}>
693
+ Confirm
694
+ </Button>
695
+ </Popover.Footer>
696
+ </Popover.Content>
697
+ </Popover.Positioner>
698
+ </Popover.Root>
699
+ );
700
+ }
701
+
702
+ // Usage
703
+ <ConfirmationPopover onConfirm={() => console.log('Deleted')}>
704
+ <Button variant="tonal">Delete</Button>
705
+ </ConfirmationPopover>
706
+ ```
707
+
708
+ ### Color Picker Popover
709
+
710
+ ```typescript
711
+ function ColorPickerPopover() {
712
+ const [color, setColor] = useState('#3b82f6');
713
+
714
+ const colors = [
715
+ '#ef4444', '#f59e0b', '#10b981', '#3b82f6',
716
+ '#8b5cf6', '#ec4899', '#64748b', '#000000',
717
+ ];
718
+
719
+ return (
720
+ <Popover.Root>
721
+ <Popover.Trigger asChild>
722
+ <button
723
+ className={css({
724
+ width: '40px',
725
+ height: '40px',
726
+ borderRadius: 'md',
727
+ border: '2px solid',
728
+ borderColor: 'gray.a7',
729
+ cursor: 'pointer',
730
+ })}
731
+ style={{ backgroundColor: color }}
732
+ aria-label="Choose color"
733
+ />
734
+ </Popover.Trigger>
735
+
736
+ <Popover.Positioner>
737
+ <Popover.Content>
738
+ <Popover.Arrow />
739
+
740
+ <Popover.Header>
741
+ <Popover.Title>Choose Color</Popover.Title>
742
+ </Popover.Header>
743
+
744
+ <Popover.Body>
745
+ <div className={css({ display: 'grid', gridTemplateColumns: '4', gap: '2' })}>
746
+ {colors.map((c) => (
747
+ <Popover.CloseTrigger key={c} asChild>
748
+ <button
749
+ onClick={() => setColor(c)}
750
+ className={css({
751
+ width: '40px',
752
+ height: '40px',
753
+ borderRadius: 'md',
754
+ border: '2px solid',
755
+ borderColor: color === c ? 'gray.12' : 'transparent',
756
+ cursor: 'pointer',
757
+ })}
758
+ style={{ backgroundColor: c }}
759
+ aria-label={`Color ${c}`}
760
+ />
761
+ </Popover.CloseTrigger>
762
+ ))}
763
+ </div>
764
+ </Popover.Body>
765
+ </Popover.Content>
766
+ </Popover.Positioner>
767
+ </Popover.Root>
768
+ );
769
+ }
770
+ ```
771
+
772
+ ### Date Picker Popover
773
+
774
+ ```typescript
775
+ function DatePickerPopover() {
776
+ const [selectedDate, setSelectedDate] = useState<Date | null>(null);
777
+
778
+ return (
779
+ <Popover.Root>
780
+ <Popover.Trigger asChild>
781
+ <Button variant="outlined" leftIcon={<CalendarIcon />}>
782
+ {selectedDate ? selectedDate.toLocaleDateString() : 'Select Date'}
783
+ </Button>
784
+ </Popover.Trigger>
785
+
786
+ <Popover.Positioner>
787
+ <Popover.Content>
788
+ <Popover.Arrow />
789
+
790
+ <Popover.Header>
791
+ <Popover.Title>Select Date</Popover.Title>
792
+ </Popover.Header>
793
+
794
+ <Popover.Body>
795
+ {/* Insert your date picker component here */}
796
+ <DatePickerComponent
797
+ value={selectedDate}
798
+ onChange={(date) => {
799
+ setSelectedDate(date);
800
+ }}
801
+ />
802
+ </Popover.Body>
803
+ </Popover.Content>
804
+ </Popover.Positioner>
805
+ </Popover.Root>
806
+ );
807
+ }
808
+ ```
809
+
810
+ ### Share Menu Popover
811
+
812
+ ```typescript
813
+ function SharePopover({ url, title }) {
814
+ const shareOptions = [
815
+ { name: 'Twitter', icon: <TwitterIcon />, action: () => window.open(`https://twitter.com/intent/tweet?url=${url}&text=${title}`) },
816
+ { name: 'Facebook', icon: <FacebookIcon />, action: () => window.open(`https://www.facebook.com/sharer/sharer.php?u=${url}`) },
817
+ { name: 'LinkedIn', icon: <LinkedInIcon />, action: () => window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${url}`) },
818
+ { name: 'Copy Link', icon: <LinkIcon />, action: () => navigator.clipboard.writeText(url) },
819
+ ];
820
+
821
+ return (
822
+ <Popover.Root>
823
+ <Popover.Trigger asChild>
824
+ <Button variant="outlined" leftIcon={<ShareIcon />}>
825
+ Share
826
+ </Button>
827
+ </Popover.Trigger>
828
+
829
+ <Popover.Positioner>
830
+ <Popover.Content>
831
+ <Popover.Arrow />
832
+
833
+ <Popover.Header>
834
+ <Popover.Title>Share this page</Popover.Title>
835
+ </Popover.Header>
836
+
837
+ <Popover.Body>
838
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: '1' })}>
839
+ {shareOptions.map((option) => (
840
+ <Popover.CloseTrigger key={option.name} asChild>
841
+ <button
842
+ onClick={option.action}
843
+ className={css({
844
+ display: 'flex',
845
+ alignItems: 'center',
846
+ gap: '3',
847
+ p: '2',
848
+ borderRadius: 'md',
849
+ bg: 'transparent',
850
+ border: 'none',
851
+ cursor: 'pointer',
852
+ width: '100%',
853
+ textAlign: 'left',
854
+ _hover: { bg: 'gray.a3' },
855
+ })}
856
+ >
857
+ {option.icon}
858
+ <span>{option.name}</span>
859
+ </button>
860
+ </Popover.CloseTrigger>
861
+ ))}
862
+ </div>
863
+ </Popover.Body>
864
+ </Popover.Content>
865
+ </Popover.Positioner>
866
+ </Popover.Root>
867
+ );
868
+ }
869
+ ```
870
+
871
+ ## DO NOT
872
+
873
+ ```typescript
874
+ // ❌ Don't forget Positioner wrapper
875
+ <Popover.Root>
876
+ <Popover.Trigger asChild>
877
+ <Button>Open</Button>
878
+ </Popover.Trigger>
879
+ <Popover.Content>...</Popover.Content> // Missing Positioner
880
+ </Popover.Root>
881
+
882
+ // ❌ Don't use for critical alerts (use Dialog instead)
883
+ <Popover.Root>
884
+ <Popover.Content>
885
+ <Popover.Title>Error: Data Lost!</Popover.Title>
886
+ <Popover.Body>Your data has been permanently deleted</Popover.Body>
887
+ </Popover.Content>
888
+ </Popover.Root>
889
+
890
+ // ❌ Don't nest popovers
891
+ <Popover.Root>
892
+ <Popover.Content>
893
+ <Popover.Root> // Don't nest
894
+ <Popover.Content>...</Popover.Content>
895
+ </Popover.Root>
896
+ </Popover.Content>
897
+ </Popover.Root>
898
+
899
+ // ❌ Don't use for long-form content (use Drawer or Dialog)
900
+ <Popover.Root>
901
+ <Popover.Content>
902
+ <Popover.Body>
903
+ {/* Multiple paragraphs of text */}
904
+ {/* Large forms */}
905
+ {/* Complex layouts */}
906
+ </Popover.Body>
907
+ </Popover.Content>
908
+ </Popover.Root>
909
+
910
+ // ❌ Don't use without proper trigger
911
+ <Popover.Root open={true}> // Always controlled without trigger
912
+ <Popover.Content>...</Popover.Content>
913
+ </Popover.Root>
914
+
915
+ // ❌ Don't omit Title when using complex content
916
+ <Popover.Content>
917
+ <Popover.Body>
918
+ <form>
919
+ {/* Complex form without title */}
920
+ </form>
921
+ </Popover.Body>
922
+ </Popover.Content>
923
+
924
+ // ❌ Don't use fixed widths (let content dictate or use positioning.sameWidth)
925
+ <Popover.Content style={{ width: '500px' }}>
926
+ ...
927
+ </Popover.Content>
928
+
929
+ // ✅ Correct usage
930
+ <Popover.Root>
931
+ <Popover.Trigger asChild>
932
+ <Button>Quick Actions</Button>
933
+ </Popover.Trigger>
934
+
935
+ <Popover.Positioner>
936
+ <Popover.Content>
937
+ <Popover.Arrow />
938
+
939
+ <Popover.Header>
940
+ <Popover.Title>Quick Actions</Popover.Title>
941
+ </Popover.Header>
942
+
943
+ <Popover.Body>
944
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: '2' })}>
945
+ <Button size="sm" variant="text">Action 1</Button>
946
+ <Button size="sm" variant="text">Action 2</Button>
947
+ </div>
948
+ </Popover.Body>
949
+ </Popover.Content>
950
+ </Popover.Positioner>
951
+ </Popover.Root>
952
+ ```
953
+
954
+ ## Accessibility
955
+
956
+ The Popover component follows WCAG 2.1 Level AA standards:
957
+
958
+ - **Focus Management**:
959
+ - Auto-focus first focusable element (configurable)
960
+ - Focus returns to trigger on close
961
+ - Focus trap optional (modal mode)
962
+ - **Keyboard Navigation**:
963
+ - `Escape` key closes popover
964
+ - `Tab` cycles through focusable elements
965
+ - Arrow keys for menu-style popovers
966
+ - **Screen Reader Support**:
967
+ - Announced as dialog or menu
968
+ - Title provides accessible name
969
+ - Description provides context
970
+ - **ARIA Attributes**:
971
+ - `role="dialog"` on Content
972
+ - `aria-haspopup="dialog"` on Trigger
973
+ - `aria-expanded` reflects open state
974
+ - `aria-labelledby` references Title
975
+ - `aria-describedby` references Description
976
+ - **Pointer Interaction**:
977
+ - Click outside closes by default
978
+ - Hover support (requires custom implementation)
979
+
980
+ ### Accessibility Best Practices
981
+
982
+ ```typescript
983
+ // ✅ Always provide Title for complex content
984
+ <Popover.Content>
985
+ <Popover.Header>
986
+ <Popover.Title>Filter Options</Popover.Title>
987
+ </Popover.Header>
988
+ <Popover.Body>
989
+ {/* Filter form */}
990
+ </Popover.Body>
991
+ </Popover.Content>
992
+
993
+ // ✅ Add Description for additional context
994
+ <Popover.Header>
995
+ <Popover.Title>Export Data</Popover.Title>
996
+ <Popover.Description>
997
+ Choose format and date range for export
998
+ </Popover.Description>
999
+ </Popover.Header>
1000
+
1001
+ // ✅ Label icon-only triggers
1002
+ <Popover.Trigger asChild>
1003
+ <IconButton aria-label="More options">
1004
+ <MoreIcon />
1005
+ </IconButton>
1006
+ </Popover.Trigger>
1007
+
1008
+ // ✅ Label close buttons
1009
+ <Popover.CloseTrigger asChild>
1010
+ <IconButton aria-label="Close popover">
1011
+ <XIcon />
1012
+ </IconButton>
1013
+ </Popover.CloseTrigger>
1014
+
1015
+ // ✅ Use proper roles for menu-style popovers
1016
+ <Popover.Body role="menu">
1017
+ <button role="menuitem">Action 1</button>
1018
+ <button role="menuitem">Action 2</button>
1019
+ </Popover.Body>
1020
+
1021
+ // ✅ Announce dynamic changes
1022
+ <Popover.Body>
1023
+ <form aria-live="polite">
1024
+ {/* Form with validation messages */}
1025
+ </form>
1026
+ </Popover.Body>
1027
+ ```
1028
+
1029
+ ## Usage Guidelines
1030
+
1031
+ ### When to Use Popover
1032
+
1033
+ | Use Case | Why Popover |
1034
+ | ------------------ | -------------------------------------------- |
1035
+ | Contextual menus | Small actions related to trigger element |
1036
+ | Quick forms | Short inputs (1-3 fields) without navigation |
1037
+ | Additional info | Tooltips with interactive content |
1038
+ | Color/date pickers | Compact UI widgets |
1039
+ | User profiles | Quick preview without full page |
1040
+ | Filter panels | Temporary filtering UI |
1041
+ | Share menus | Social sharing options |
1042
+
1043
+ ### When NOT to Use Popover
1044
+
1045
+ | Use Case | Use Instead | Why |
1046
+ | ------------------ | --------------- | --------------------------------------- |
1047
+ | Critical alerts | Dialog | Center attention, harder to dismiss |
1048
+ | Long forms | Drawer/Page | More space, better UX for complex input |
1049
+ | Simple hints | Tooltip | Popover is too heavy for read-only text |
1050
+ | Primary navigation | Drawer/Menu | Better for main nav structure |
1051
+ | Full content | Page/Route | Popover is temporary/contextual |
1052
+ | Mobile sheets | Drawer (bottom) | Better mobile UX |
1053
+
1054
+ ## Placement Guidelines
1055
+
1056
+ ### Placement Options
1057
+
1058
+ | Placement | Best For | Arrow Position |
1059
+ | -------------- | ------------------------------- | ----------------- |
1060
+ | `top` | Content above trigger | Points down |
1061
+ | `top-start` | Left-aligned top | Points down-right |
1062
+ | `top-end` | Right-aligned top | Points down-left |
1063
+ | `bottom` | Content below trigger (default) | Points up |
1064
+ | `bottom-start` | Left-aligned bottom | Points up-right |
1065
+ | `bottom-end` | Right-aligned bottom | Points up-left |
1066
+ | `left` | Content left of trigger | Points right |
1067
+ | `left-start` | Top-aligned left | Points right-down |
1068
+ | `left-end` | Bottom-aligned left | Points right-up |
1069
+ | `right` | Content right of trigger | Points left |
1070
+ | `right-start` | Top-aligned right | Points left-down |
1071
+ | `right-end` | Bottom-aligned right | Points left-up |
1072
+
1073
+ **Auto-positioning**: Popover automatically flips to the opposite side if there's insufficient space.
1074
+
1075
+ ## Content Guidelines
1076
+
1077
+ ### Size Recommendations
1078
+
1079
+ | Content Type | Max Width | Max Height |
1080
+ | ------------------- | ---------- | ---------- |
1081
+ | Action menu | 280px | auto |
1082
+ | Quick form | 384px (sm) | auto |
1083
+ | Info panel | 384px (sm) | 400px |
1084
+ | Rich content | 480px | 500px |
1085
+ | Picker (color/date) | auto | auto |
1086
+
1087
+ ### Content Structure
1088
+
1089
+ ```typescript
1090
+ // Simple (no header/footer)
1091
+ <Popover.Content>
1092
+ <Popover.Arrow />
1093
+ <Popover.Body>
1094
+ Simple content
1095
+ </Popover.Body>
1096
+ </Popover.Content>
1097
+
1098
+ // Standard (with header)
1099
+ <Popover.Content>
1100
+ <Popover.Arrow />
1101
+ <Popover.Header>
1102
+ <Popover.Title>Title</Popover.Title>
1103
+ </Popover.Header>
1104
+ <Popover.Body>
1105
+ Content
1106
+ </Popover.Body>
1107
+ </Popover.Content>
1108
+
1109
+ // Complete (header + footer)
1110
+ <Popover.Content>
1111
+ <Popover.Arrow />
1112
+ <Popover.Header>
1113
+ <Popover.Title>Title</Popover.Title>
1114
+ <Popover.Description>Description</Popover.Description>
1115
+ </Popover.Header>
1116
+ <Popover.Body>
1117
+ Content
1118
+ </Popover.Body>
1119
+ <Popover.Footer>
1120
+ <Button>Action</Button>
1121
+ </Popover.Footer>
1122
+ </Popover.Content>
1123
+ ```
1124
+
1125
+ ## State Behaviors
1126
+
1127
+ | State | Visual Change | Behavior |
1128
+ | ----------------- | --------------------- | ------------------------ |
1129
+ | **Opening** | Scale up + fade in | Duration: fast (150ms) |
1130
+ | **Open** | Fully visible | Auto-focus first element |
1131
+ | **Closing** | Scale down + fade out | Duration: faster (100ms) |
1132
+ | **Closed** | Removed from DOM | Focus returns to trigger |
1133
+ | **Repositioning** | Smooth transition | Follows scroll/resize |
1134
+
1135
+ ## Testing
1136
+
1137
+ When testing Popover components:
1138
+
1139
+ ```typescript
1140
+ import { render, screen, waitFor } from '@testing-library/react';
1141
+ import userEvent from '@testing-library/user-event';
1142
+ import { Popover, Button } from '@discourser/design-system';
1143
+
1144
+ test('popover opens and closes', async () => {
1145
+ const user = userEvent.setup();
1146
+
1147
+ render(
1148
+ <Popover.Root>
1149
+ <Popover.Trigger asChild>
1150
+ <Button>Open</Button>
1151
+ </Popover.Trigger>
1152
+ <Popover.Positioner>
1153
+ <Popover.Content>
1154
+ <Popover.Title>Test Popover</Popover.Title>
1155
+ <Popover.Body>Content</Popover.Body>
1156
+ <Popover.CloseTrigger asChild>
1157
+ <Button>Close</Button>
1158
+ </Popover.CloseTrigger>
1159
+ </Popover.Content>
1160
+ </Popover.Positioner>
1161
+ </Popover.Root>
1162
+ );
1163
+
1164
+ // Initially closed
1165
+ expect(screen.queryByText('Test Popover')).not.toBeInTheDocument();
1166
+
1167
+ // Open popover
1168
+ await user.click(screen.getByText('Open'));
1169
+
1170
+ await waitFor(() => {
1171
+ expect(screen.getByText('Test Popover')).toBeInTheDocument();
1172
+ });
1173
+
1174
+ // Close popover
1175
+ await user.click(screen.getByText('Close'));
1176
+
1177
+ await waitFor(() => {
1178
+ expect(screen.queryByText('Test Popover')).not.toBeInTheDocument();
1179
+ });
1180
+ });
1181
+
1182
+ test('popover closes on outside click', async () => {
1183
+ const user = userEvent.setup();
1184
+
1185
+ render(
1186
+ <div>
1187
+ <button>Outside</button>
1188
+ <Popover.Root>
1189
+ <Popover.Trigger asChild>
1190
+ <Button>Open</Button>
1191
+ </Popover.Trigger>
1192
+ <Popover.Positioner>
1193
+ <Popover.Content>
1194
+ <Popover.Title>Test</Popover.Title>
1195
+ </Popover.Content>
1196
+ </Popover.Positioner>
1197
+ </Popover.Root>
1198
+ </div>
1199
+ );
1200
+
1201
+ await user.click(screen.getByText('Open'));
1202
+ expect(screen.getByText('Test')).toBeInTheDocument();
1203
+
1204
+ await user.click(screen.getByText('Outside'));
1205
+
1206
+ await waitFor(() => {
1207
+ expect(screen.queryByText('Test')).not.toBeInTheDocument();
1208
+ });
1209
+ });
1210
+
1211
+ test('popover closes on escape key', async () => {
1212
+ const user = userEvent.setup();
1213
+
1214
+ render(
1215
+ <Popover.Root defaultOpen>
1216
+ <Popover.Positioner>
1217
+ <Popover.Content>
1218
+ <Popover.Title>Test</Popover.Title>
1219
+ </Popover.Content>
1220
+ </Popover.Positioner>
1221
+ </Popover.Root>
1222
+ );
1223
+
1224
+ expect(screen.getByText('Test')).toBeInTheDocument();
1225
+
1226
+ await user.keyboard('{Escape}');
1227
+
1228
+ await waitFor(() => {
1229
+ expect(screen.queryByText('Test')).not.toBeInTheDocument();
1230
+ });
1231
+ });
1232
+
1233
+ test('controlled popover updates on state change', async () => {
1234
+ const user = userEvent.setup();
1235
+
1236
+ function ControlledTest() {
1237
+ const [open, setOpen] = useState(false);
1238
+ return (
1239
+ <>
1240
+ <button onClick={() => setOpen(true)}>External Open</button>
1241
+ <Popover.Root open={open} onOpenChange={(e) => setOpen(e.open)}>
1242
+ <Popover.Positioner>
1243
+ <Popover.Content>
1244
+ <Popover.Title>Controlled</Popover.Title>
1245
+ </Popover.Content>
1246
+ </Popover.Positioner>
1247
+ </Popover.Root>
1248
+ </>
1249
+ );
1250
+ }
1251
+
1252
+ render(<ControlledTest />);
1253
+
1254
+ expect(screen.queryByText('Controlled')).not.toBeInTheDocument();
1255
+
1256
+ await user.click(screen.getByText('External Open'));
1257
+
1258
+ await waitFor(() => {
1259
+ expect(screen.getByText('Controlled')).toBeInTheDocument();
1260
+ });
1261
+ });
1262
+ ```
1263
+
1264
+ ## Related Components
1265
+
1266
+ - **Tooltip**: Use for simple, non-interactive hints
1267
+ - **Menu**: Use for dropdown menus with keyboard navigation
1268
+ - **Drawer**: Use for larger panels or mobile sheets
1269
+ - **Dialog**: Use for centered modals and critical alerts
1270
+ - **Select**: Use for form dropdowns with options
1271
+ - **Dropdown**: Use for trigger-based menus