@proteos/ui 0.45.0 → 0.47.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 (43) hide show
  1. package/dist/index.cjs +720 -70
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +191 -8
  4. package/dist/index.d.ts +191 -8
  5. package/dist/index.js +715 -74
  6. package/dist/index.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/components/attribute-type-icon/attribute-type-icon.tsx +5 -0
  9. package/src/components/avatar/avatar.stories.tsx +37 -1
  10. package/src/components/avatar/avatar.test.tsx +42 -0
  11. package/src/components/avatar/avatar.tsx +39 -4
  12. package/src/components/avatar-group/avatar-group.test.tsx +19 -0
  13. package/src/components/avatar-group/avatar-group.tsx +18 -3
  14. package/src/components/badge/badge.stories.tsx +27 -1
  15. package/src/components/badge/badge.test.tsx +26 -0
  16. package/src/components/badge/badge.tsx +48 -4
  17. package/src/components/badge-multi-select/badge-multi-select.stories.tsx +24 -0
  18. package/src/components/badge-multi-select/badge-multi-select.tsx +25 -4
  19. package/src/components/checkbox/checkbox.tsx +8 -1
  20. package/src/components/color-input/color-input.stories.tsx +72 -0
  21. package/src/components/color-input/color-input.test.tsx +95 -0
  22. package/src/components/color-input/color-input.tsx +208 -0
  23. package/src/components/color-input/index.ts +2 -0
  24. package/src/components/data-table/data-table.test.tsx +112 -0
  25. package/src/components/data-table/data-table.tsx +135 -11
  26. package/src/components/data-table/index.ts +1 -1
  27. package/src/components/draft-message-composer/draft-message-composer.stories.tsx +1 -1
  28. package/src/components/icon-picker/icon-picker.stories.tsx +77 -0
  29. package/src/components/icon-picker/icon-picker.test.tsx +88 -0
  30. package/src/components/icon-picker/icon-picker.tsx +336 -0
  31. package/src/components/icon-picker/index.ts +2 -0
  32. package/src/components/record-filter/chip-value.tsx +10 -3
  33. package/src/components/record-filter/filter-value-input.tsx +12 -2
  34. package/src/components/record-filter/operators.ts +6 -6
  35. package/src/components/record-filter/record-filter.stories.tsx +1 -1
  36. package/src/components/record-filter/types.ts +4 -0
  37. package/src/components/table/table.stories.tsx +12 -13
  38. package/src/components/toast/index.ts +9 -0
  39. package/src/components/toast/toast.stories.tsx +58 -0
  40. package/src/components/toast/toast.test.tsx +34 -0
  41. package/src/components/toast/toast.tsx +121 -0
  42. package/src/index.ts +3 -0
  43. package/src/test/setup.ts +30 -0
@@ -0,0 +1,9 @@
1
+ export type {
2
+ ToastContent,
3
+ ToastOptions,
4
+ ToastQueue,
5
+ ToastRegionProps,
6
+ ToastVariant,
7
+ ToastVariants,
8
+ } from './toast'
9
+ export { createToastQueue, ToastRegion, toastStyles } from './toast'
@@ -0,0 +1,58 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+
3
+ import { Button } from '@/components/button/button'
4
+
5
+ import { createToastQueue, ToastRegion } from './toast'
6
+
7
+ const queue = createToastQueue()
8
+
9
+ const meta: Meta<typeof ToastRegion> = {
10
+ title: 'Components/Toast',
11
+ component: ToastRegion,
12
+ tags: ['autodocs'],
13
+ parameters: { layout: 'padded' },
14
+ }
15
+
16
+ export default meta
17
+ type Story = StoryObj<typeof ToastRegion>
18
+
19
+ export const Basic: Story = {
20
+ render: () => (
21
+ <div className="flex gap-2">
22
+ <Button
23
+ variant="secondary"
24
+ onPress={() => queue.add({ title: 'Saved', variant: 'info' }, { timeout: 4000 })}
25
+ >
26
+ Info
27
+ </Button>
28
+ <Button
29
+ variant="secondary"
30
+ onPress={() =>
31
+ queue.add(
32
+ {
33
+ title: 'Workflow finished',
34
+ description: 'sync-hubspot succeeded in 0:12',
35
+ variant: 'success',
36
+ },
37
+ { timeout: 5000 },
38
+ )
39
+ }
40
+ >
41
+ Success
42
+ </Button>
43
+ <Button
44
+ variant="secondary"
45
+ onPress={() =>
46
+ queue.add({
47
+ title: 'Workflow failed',
48
+ description: 'node "Create deal": hubspot returned 429 rate_limited',
49
+ variant: 'danger',
50
+ })
51
+ }
52
+ >
53
+ Danger (sticky)
54
+ </Button>
55
+ <ToastRegion queue={queue} />
56
+ </div>
57
+ ),
58
+ }
@@ -0,0 +1,34 @@
1
+ import { act, render, screen } from '@testing-library/react'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { describe, expect, it } from 'vitest'
4
+
5
+ import { createToastQueue, ToastRegion } from './toast'
6
+
7
+ describe('Toast', () => {
8
+ it('renders nothing until a toast is queued, then shows title + description', () => {
9
+ const queue = createToastQueue()
10
+ render(<ToastRegion queue={queue} />)
11
+ expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
12
+
13
+ act(() => {
14
+ queue.add({
15
+ title: 'Workflow finished',
16
+ description: 'sync-hubspot succeeded',
17
+ variant: 'success',
18
+ })
19
+ })
20
+ expect(screen.getByText('Workflow finished')).toBeInTheDocument()
21
+ expect(screen.getByText('sync-hubspot succeeded')).toBeInTheDocument()
22
+ })
23
+
24
+ it('dismisses via the close button', async () => {
25
+ const queue = createToastQueue()
26
+ render(<ToastRegion queue={queue} />)
27
+ act(() => {
28
+ queue.add({ title: 'Hello' })
29
+ })
30
+ const user = userEvent.setup()
31
+ await user.click(screen.getByRole('button', { name: 'Dismiss' }))
32
+ expect(screen.queryByText('Hello')).not.toBeInTheDocument()
33
+ })
34
+ })
@@ -0,0 +1,121 @@
1
+ import { CircleAlert, CircleCheck, Info, X as XLucide } from 'lucide-react'
2
+ import {
3
+ Button as AriaButton,
4
+ Text as AriaText,
5
+ UNSTABLE_Toast as AriaToast,
6
+ UNSTABLE_ToastContent as AriaToastContent,
7
+ type ToastOptions as AriaToastOptions,
8
+ UNSTABLE_ToastQueue as AriaToastQueue,
9
+ UNSTABLE_ToastRegion as AriaToastRegion,
10
+ } from 'react-aria-components'
11
+ import { tv, type VariantProps } from 'tailwind-variants'
12
+
13
+ import { cn } from '../../lib/cn'
14
+
15
+ /* =========================================================================
16
+ Toast — brief, transient notification stack (bottom-right). Wraps RAC's
17
+ ToastRegion / Toast (queue, ARIA live region, focus management, timeouts).
18
+
19
+ Usage: create ONE queue per app (`createToastQueue()`), mount ONE
20
+ `<ToastRegion queue={queue} />` near the root, and push from anywhere
21
+ with `queue.add({ title, description?, variant? }, { timeout })`.
22
+ ========================================================================= */
23
+
24
+ export type ToastVariant = 'info' | 'success' | 'danger'
25
+
26
+ /** The payload a caller enqueues. Pure data — the region renders it. */
27
+ export interface ToastContent {
28
+ title: string
29
+ description?: string
30
+ variant?: ToastVariant
31
+ }
32
+
33
+ export type ToastQueue = AriaToastQueue<ToastContent>
34
+ export type ToastOptions = AriaToastOptions
35
+
36
+ /** Creates the app-wide queue. `maxVisibleToasts` bounds the on-screen stack. */
37
+ export function createToastQueue(options: { maxVisibleToasts?: number } = {}): ToastQueue {
38
+ return new AriaToastQueue<ToastContent>({ maxVisibleToasts: options.maxVisibleToasts ?? 3 })
39
+ }
40
+
41
+ export const toastStyles = tv({
42
+ slots: {
43
+ root: [
44
+ 'flex items-start gap-3 w-[360px] max-w-[calc(100vw-32px)]',
45
+ 'rounded-lg border border-rule bg-surface shadow-float',
46
+ 'px-3.5 py-3 outline-none',
47
+ 'focus-visible:shadow-focus',
48
+ 'entering:animate-in entering:fade-in-0 entering:slide-in-from-bottom-2 entering:duration-200',
49
+ 'exiting:animate-out exiting:fade-out-0 exiting:duration-150',
50
+ ],
51
+ icon: 'size-4 shrink-0 mt-0.5',
52
+ title: 'font-sans text-[13px] font-medium leading-[1.4] text-ink',
53
+ description: 'font-sans text-[12.5px] leading-[1.5] text-ink-3 break-words',
54
+ },
55
+ variants: {
56
+ variant: {
57
+ info: { icon: 'text-ink-3' },
58
+ success: { icon: 'text-success' },
59
+ danger: { icon: 'text-danger' },
60
+ },
61
+ },
62
+ defaultVariants: { variant: 'info' },
63
+ })
64
+
65
+ export type ToastVariants = VariantProps<typeof toastStyles>
66
+
67
+ const VARIANT_ICON = {
68
+ info: Info,
69
+ success: CircleCheck,
70
+ danger: CircleAlert,
71
+ } as const
72
+
73
+ export interface ToastRegionProps {
74
+ queue: ToastQueue
75
+ className?: string
76
+ }
77
+
78
+ /** The single live region that renders queued toasts. */
79
+ export function ToastRegion({ queue, className }: ToastRegionProps) {
80
+ return (
81
+ <AriaToastRegion
82
+ queue={queue}
83
+ className={cn(
84
+ 'fixed bottom-4 right-4 z-[1300] flex flex-col-reverse gap-2 outline-none',
85
+ className,
86
+ )}
87
+ >
88
+ {({ toast }) => {
89
+ const variant = toast.content.variant ?? 'info'
90
+ const Icon = VARIANT_ICON[variant]
91
+ const { root, icon, title, description } = toastStyles({ variant })
92
+ return (
93
+ <AriaToast toast={toast} className={root()}>
94
+ <Icon className={icon()} aria-hidden />
95
+ <AriaToastContent className="flex flex-col gap-0.5 flex-1 min-w-0">
96
+ <AriaText slot="title" className={title()}>
97
+ {toast.content.title}
98
+ </AriaText>
99
+ {toast.content.description ? (
100
+ <AriaText slot="description" className={description()}>
101
+ {toast.content.description}
102
+ </AriaText>
103
+ ) : null}
104
+ </AriaToastContent>
105
+ <AriaButton
106
+ slot="close"
107
+ aria-label="Dismiss"
108
+ className={cn(
109
+ 'shrink-0 inline-flex items-center justify-center size-6 rounded-md',
110
+ 'text-ink-4 hover:text-ink hover:bg-surface-2 cursor-pointer outline-none',
111
+ 'focus-visible:shadow-focus',
112
+ )}
113
+ >
114
+ <XLucide className="size-3.5" />
115
+ </AriaButton>
116
+ </AriaToast>
117
+ )
118
+ }}
119
+ </AriaToastRegion>
120
+ )
121
+ }
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export * from './components/button'
9
9
  export * from './components/card'
10
10
  export * from './components/channel-icon'
11
11
  export * from './components/checkbox'
12
+ export * from './components/color-input'
12
13
  export * from './components/combobox'
13
14
  export * from './components/command'
14
15
  export * from './components/currency-input'
@@ -22,6 +23,7 @@ export * from './components/error-animation'
22
23
  export * from './components/field'
23
24
  export * from './components/file-field'
24
25
  export * from './components/file-viewer'
26
+ export * from './components/icon-picker'
25
27
  export * from './components/image-attachment'
26
28
  export * from './components/image-lightbox'
27
29
  export * from './components/input'
@@ -47,6 +49,7 @@ export * from './components/tabs'
47
49
  export * from './components/tag-input'
48
50
  export * from './components/textarea'
49
51
  export * from './components/time-field'
52
+ export * from './components/toast'
50
53
  export * from './components/tooltip'
51
54
 
52
55
  export { cn } from './lib/cn'
package/src/test/setup.ts CHANGED
@@ -4,6 +4,36 @@ import { afterEach, expect } from 'vitest'
4
4
 
5
5
  expect.extend(matchers)
6
6
 
7
+ // jsdom implements neither Element.scrollTo nor IntersectionObserver, both of
8
+ // which the virtualized DataTable calls on mount. Stub them so scrolling
9
+ // components can be rendered in tests at all.
10
+ if (!Element.prototype.scrollTo) {
11
+ Element.prototype.scrollTo = () => {}
12
+ }
13
+ if (!('ResizeObserver' in globalThis)) {
14
+ class NoopResizeObserver implements ResizeObserver {
15
+ disconnect() {}
16
+ observe() {}
17
+ unobserve() {}
18
+ }
19
+ globalThis.ResizeObserver = NoopResizeObserver as unknown as typeof ResizeObserver
20
+ }
21
+ if (!('IntersectionObserver' in globalThis)) {
22
+ class NoopIntersectionObserver implements IntersectionObserver {
23
+ readonly root = null
24
+ readonly rootMargin = ''
25
+ readonly thresholds: ReadonlyArray<number> = []
26
+ disconnect() {}
27
+ observe() {}
28
+ unobserve() {}
29
+ takeRecords(): IntersectionObserverEntry[] {
30
+ return []
31
+ }
32
+ }
33
+ globalThis.IntersectionObserver =
34
+ NoopIntersectionObserver as unknown as typeof IntersectionObserver
35
+ }
36
+
7
37
  afterEach(() => {
8
38
  cleanup()
9
39
  })