@proteos/ui 0.31.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 (226) hide show
  1. package/LICENSE +105 -0
  2. package/dist/chunk-IKAX76MB.js +113 -0
  3. package/dist/chunk-IKAX76MB.js.map +1 -0
  4. package/dist/chunk-MWAXK66H.cjs +119 -0
  5. package/dist/chunk-MWAXK66H.cjs.map +1 -0
  6. package/dist/index.cjs +6246 -0
  7. package/dist/index.cjs.map +1 -0
  8. package/dist/index.d.cts +2126 -0
  9. package/dist/index.d.ts +2126 -0
  10. package/dist/index.js +6069 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/pdf-view-3ZMMS6ML.cjs +95 -0
  13. package/dist/pdf-view-3ZMMS6ML.cjs.map +1 -0
  14. package/dist/pdf-view-BV5ZD6OC.js +93 -0
  15. package/dist/pdf-view-BV5ZD6OC.js.map +1 -0
  16. package/package.json +96 -0
  17. package/src/components/attachment-chip/attachment-chip.tsx +129 -0
  18. package/src/components/attachment-chip/index.ts +2 -0
  19. package/src/components/attribute-type-icon/attribute-type-icon.stories.tsx +70 -0
  20. package/src/components/attribute-type-icon/attribute-type-icon.tsx +125 -0
  21. package/src/components/attribute-type-icon/index.ts +8 -0
  22. package/src/components/avatar/avatar.stories.tsx +42 -0
  23. package/src/components/avatar/avatar.tsx +48 -0
  24. package/src/components/avatar/index.ts +2 -0
  25. package/src/components/avatar-group/avatar-group.test.tsx +38 -0
  26. package/src/components/avatar-group/avatar-group.tsx +60 -0
  27. package/src/components/avatar-group/index.ts +2 -0
  28. package/src/components/badge/badge.stories.tsx +62 -0
  29. package/src/components/badge/badge.test.tsx +36 -0
  30. package/src/components/badge/badge.tsx +54 -0
  31. package/src/components/badge/index.ts +2 -0
  32. package/src/components/badge-multi-select/badge-multi-select.stories.tsx +177 -0
  33. package/src/components/badge-multi-select/badge-multi-select.test.tsx +155 -0
  34. package/src/components/badge-multi-select/badge-multi-select.tsx +272 -0
  35. package/src/components/badge-multi-select/index.ts +2 -0
  36. package/src/components/breadcrumbs/breadcrumbs.stories.tsx +43 -0
  37. package/src/components/breadcrumbs/breadcrumbs.tsx +86 -0
  38. package/src/components/breadcrumbs/index.ts +2 -0
  39. package/src/components/button/button.stories.tsx +290 -0
  40. package/src/components/button/button.test.tsx +59 -0
  41. package/src/components/button/button.tsx +86 -0
  42. package/src/components/button/index.ts +2 -0
  43. package/src/components/card/card.stories.tsx +351 -0
  44. package/src/components/card/card.test.tsx +141 -0
  45. package/src/components/card/card.tsx +173 -0
  46. package/src/components/card/index.ts +13 -0
  47. package/src/components/checkbox/checkbox-group.tsx +52 -0
  48. package/src/components/checkbox/checkbox.stories.tsx +48 -0
  49. package/src/components/checkbox/checkbox.test.tsx +50 -0
  50. package/src/components/checkbox/checkbox.tsx +71 -0
  51. package/src/components/checkbox/index.ts +4 -0
  52. package/src/components/combobox/combobox.stories.tsx +97 -0
  53. package/src/components/combobox/combobox.test.tsx +123 -0
  54. package/src/components/combobox/combobox.tsx +239 -0
  55. package/src/components/combobox/index.ts +2 -0
  56. package/src/components/command/command-hint.tsx +46 -0
  57. package/src/components/command/command.stories.tsx +88 -0
  58. package/src/components/command/command.test.tsx +102 -0
  59. package/src/components/command/command.tsx +76 -0
  60. package/src/components/command/index.ts +16 -0
  61. package/src/components/currency-input/currency-input.tsx +192 -0
  62. package/src/components/currency-input/index.ts +6 -0
  63. package/src/components/data-table/cells.stories.tsx +120 -0
  64. package/src/components/data-table/cells.tsx +165 -0
  65. package/src/components/data-table/data-table.stories.tsx +371 -0
  66. package/src/components/data-table/data-table.tsx +556 -0
  67. package/src/components/data-table/index.ts +22 -0
  68. package/src/components/data-table/make-actions-column.ts +30 -0
  69. package/src/components/data-table/states/data-table-empty.tsx +40 -0
  70. package/src/components/data-table/states/data-table-error.tsx +30 -0
  71. package/src/components/data-table/states/data-table-skeleton.tsx +42 -0
  72. package/src/components/data-table/use-infinite-scroll-sentinel.ts +51 -0
  73. package/src/components/date-picker/calendar-internals.tsx +138 -0
  74. package/src/components/date-picker/date-picker.stories.tsx +78 -0
  75. package/src/components/date-picker/date-picker.test.tsx +71 -0
  76. package/src/components/date-picker/date-picker.tsx +140 -0
  77. package/src/components/date-picker/index.ts +2 -0
  78. package/src/components/date-time-picker/date-time-picker.stories.tsx +96 -0
  79. package/src/components/date-time-picker/date-time-picker.test.tsx +87 -0
  80. package/src/components/date-time-picker/date-time-picker.tsx +203 -0
  81. package/src/components/date-time-picker/index.ts +2 -0
  82. package/src/components/empty-state/empty-state.tsx +63 -0
  83. package/src/components/empty-state/index.ts +2 -0
  84. package/src/components/error-animation/error-animation.stories.tsx +50 -0
  85. package/src/components/error-animation/error-animation.test.tsx +22 -0
  86. package/src/components/error-animation/error-animation.tsx +26 -0
  87. package/src/components/error-animation/index.ts +2 -0
  88. package/src/components/field/field.tsx +142 -0
  89. package/src/components/field/index.ts +8 -0
  90. package/src/components/file-field/file-field.tsx +285 -0
  91. package/src/components/file-field/index.ts +2 -0
  92. package/src/components/file-viewer/fallback-card.tsx +68 -0
  93. package/src/components/file-viewer/file-viewer.stories.tsx +67 -0
  94. package/src/components/file-viewer/file-viewer.test.tsx +81 -0
  95. package/src/components/file-viewer/file-viewer.tsx +116 -0
  96. package/src/components/file-viewer/image-view.tsx +38 -0
  97. package/src/components/file-viewer/index.ts +7 -0
  98. package/src/components/file-viewer/pdf-view.tsx +122 -0
  99. package/src/components/file-viewer/video-view.tsx +16 -0
  100. package/src/components/image-attachment/image-attachment.stories.tsx +39 -0
  101. package/src/components/image-attachment/image-attachment.test.tsx +54 -0
  102. package/src/components/image-attachment/image-attachment.tsx +77 -0
  103. package/src/components/image-attachment/index.ts +3 -0
  104. package/src/components/image-attachment/is-previewable-image-name.ts +14 -0
  105. package/src/components/image-lightbox/image-lightbox.stories.tsx +38 -0
  106. package/src/components/image-lightbox/image-lightbox.test.tsx +72 -0
  107. package/src/components/image-lightbox/image-lightbox.tsx +92 -0
  108. package/src/components/image-lightbox/index.ts +2 -0
  109. package/src/components/input/index.ts +2 -0
  110. package/src/components/input/input.stories.tsx +71 -0
  111. package/src/components/input/input.test.tsx +36 -0
  112. package/src/components/input/input.tsx +76 -0
  113. package/src/components/kbd/index.ts +2 -0
  114. package/src/components/kbd/kbd.stories.tsx +34 -0
  115. package/src/components/kbd/kbd.tsx +27 -0
  116. package/src/components/menu/index.ts +2 -0
  117. package/src/components/menu/menu.tsx +121 -0
  118. package/src/components/modal/index.ts +10 -0
  119. package/src/components/modal/modal.stories.tsx +153 -0
  120. package/src/components/modal/modal.test.tsx +93 -0
  121. package/src/components/modal/modal.tsx +149 -0
  122. package/src/components/multi-select/index.ts +2 -0
  123. package/src/components/multi-select/multi-select.stories.tsx +211 -0
  124. package/src/components/multi-select/multi-select.test.tsx +151 -0
  125. package/src/components/multi-select/multi-select.tsx +339 -0
  126. package/src/components/nav/index.ts +7 -0
  127. package/src/components/nav/nav.stories.tsx +139 -0
  128. package/src/components/nav/nav.tsx +246 -0
  129. package/src/components/popover/index.ts +8 -0
  130. package/src/components/popover/popover.stories.tsx +92 -0
  131. package/src/components/popover/popover.tsx +135 -0
  132. package/src/components/progress-bar/index.ts +2 -0
  133. package/src/components/progress-bar/progress-bar.stories.tsx +75 -0
  134. package/src/components/progress-bar/progress-bar.test.tsx +29 -0
  135. package/src/components/progress-bar/progress-bar.tsx +57 -0
  136. package/src/components/radio-group/index.ts +2 -0
  137. package/src/components/radio-group/radio-group.stories.tsx +113 -0
  138. package/src/components/radio-group/radio-group.test.tsx +87 -0
  139. package/src/components/radio-group/radio-group.tsx +104 -0
  140. package/src/components/record-filter/attribute-picker.tsx +74 -0
  141. package/src/components/record-filter/chip-value.tsx +171 -0
  142. package/src/components/record-filter/filter-row.tsx +262 -0
  143. package/src/components/record-filter/filter-value-input.tsx +351 -0
  144. package/src/components/record-filter/group-add-controls.tsx +77 -0
  145. package/src/components/record-filter/group-body.tsx +173 -0
  146. package/src/components/record-filter/group-mutations.test.ts +104 -0
  147. package/src/components/record-filter/group-mutations.ts +116 -0
  148. package/src/components/record-filter/index.ts +58 -0
  149. package/src/components/record-filter/nested-group.tsx +138 -0
  150. package/src/components/record-filter/operator-picker.tsx +62 -0
  151. package/src/components/record-filter/operators.test.ts +105 -0
  152. package/src/components/record-filter/operators.ts +97 -0
  153. package/src/components/record-filter/record-filter-builder-panel.tsx +236 -0
  154. package/src/components/record-filter/record-filter-button.tsx +126 -0
  155. package/src/components/record-filter/record-filter-chips.tsx +527 -0
  156. package/src/components/record-filter/record-filter-editor.tsx +91 -0
  157. package/src/components/record-filter/record-filter.stories.tsx +196 -0
  158. package/src/components/record-filter/record-filter.test.tsx +437 -0
  159. package/src/components/record-filter/show-details-store.ts +72 -0
  160. package/src/components/record-filter/types.ts +150 -0
  161. package/src/components/segmented-control/index.ts +6 -0
  162. package/src/components/segmented-control/segmented-control.stories.tsx +67 -0
  163. package/src/components/segmented-control/segmented-control.test.tsx +47 -0
  164. package/src/components/segmented-control/segmented-control.tsx +105 -0
  165. package/src/components/select/index.ts +2 -0
  166. package/src/components/select/select.stories.tsx +76 -0
  167. package/src/components/select/select.test.tsx +56 -0
  168. package/src/components/select/select.tsx +159 -0
  169. package/src/components/separator/index.ts +2 -0
  170. package/src/components/separator/separator.stories.tsx +73 -0
  171. package/src/components/separator/separator.test.tsx +29 -0
  172. package/src/components/separator/separator.tsx +51 -0
  173. package/src/components/skeleton/index.ts +2 -0
  174. package/src/components/skeleton/skeleton.stories.tsx +68 -0
  175. package/src/components/skeleton/skeleton.test.tsx +38 -0
  176. package/src/components/skeleton/skeleton.tsx +43 -0
  177. package/src/components/success-animation/index.ts +2 -0
  178. package/src/components/success-animation/success-animation.stories.tsx +50 -0
  179. package/src/components/success-animation/success-animation.test.tsx +22 -0
  180. package/src/components/success-animation/success-animation.tsx +26 -0
  181. package/src/components/switch/index.ts +2 -0
  182. package/src/components/switch/switch.stories.tsx +60 -0
  183. package/src/components/switch/switch.test.tsx +50 -0
  184. package/src/components/switch/switch.tsx +49 -0
  185. package/src/components/table/index.ts +25 -0
  186. package/src/components/table/table.stories.tsx +306 -0
  187. package/src/components/table/table.test.tsx +115 -0
  188. package/src/components/table/table.tsx +543 -0
  189. package/src/components/tabs/index.ts +8 -0
  190. package/src/components/tabs/tabs.tsx +141 -0
  191. package/src/components/tag-input/index.ts +2 -0
  192. package/src/components/tag-input/tag-input.stories.tsx +73 -0
  193. package/src/components/tag-input/tag-input.test.tsx +80 -0
  194. package/src/components/tag-input/tag-input.tsx +193 -0
  195. package/src/components/textarea/index.ts +2 -0
  196. package/src/components/textarea/textarea.stories.tsx +75 -0
  197. package/src/components/textarea/textarea.test.tsx +41 -0
  198. package/src/components/textarea/textarea.tsx +86 -0
  199. package/src/components/time-field/index.ts +2 -0
  200. package/src/components/time-field/time-field.stories.tsx +76 -0
  201. package/src/components/time-field/time-field.test.tsx +51 -0
  202. package/src/components/time-field/time-field.tsx +98 -0
  203. package/src/components/tooltip/index.ts +2 -0
  204. package/src/components/tooltip/tooltip.stories.tsx +176 -0
  205. package/src/components/tooltip/tooltip.test.tsx +52 -0
  206. package/src/components/tooltip/tooltip.tsx +56 -0
  207. package/src/index.ts +47 -0
  208. package/src/lib/cn.ts +6 -0
  209. package/src/lib/commands/discovery-store.test.tsx +76 -0
  210. package/src/lib/commands/discovery-store.ts +110 -0
  211. package/src/lib/commands/index.ts +9 -0
  212. package/src/lib/commands/match-event.test.ts +99 -0
  213. package/src/lib/commands/match-event.ts +31 -0
  214. package/src/lib/commands/parse-combo.test.ts +65 -0
  215. package/src/lib/commands/parse-combo.ts +48 -0
  216. package/src/lib/commands/platform.ts +27 -0
  217. package/src/lib/commands/protected-combos.test.ts +18 -0
  218. package/src/lib/commands/protected-combos.ts +38 -0
  219. package/src/lib/commands/registry.test.ts +97 -0
  220. package/src/lib/commands/registry.ts +59 -0
  221. package/src/lib/commands/use-command.test.tsx +91 -0
  222. package/src/lib/commands/use-command.ts +32 -0
  223. package/src/lib/commands/use-escape-key.test.tsx +97 -0
  224. package/src/lib/commands/use-escape-key.ts +62 -0
  225. package/src/styles.css +321 -0
  226. package/src/test/setup.ts +9 -0
@@ -0,0 +1,42 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+
3
+ import { Avatar } from './avatar'
4
+
5
+ const meta: Meta<typeof Avatar> = {
6
+ title: 'Components/Avatar',
7
+ component: Avatar,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] },
11
+ tone: { control: 'inline-radio', options: ['paper', 'indigo', 'moss', 'terra', 'slate'] },
12
+ },
13
+ }
14
+
15
+ export default meta
16
+ type Story = StoryObj<typeof Avatar>
17
+
18
+ export const Playground: Story = { args: { initials: 'JM' } }
19
+
20
+ export const Tones: Story = {
21
+ parameters: { controls: { hideNoControlsWarning: true } },
22
+ render: () => (
23
+ <div className="flex items-center gap-3">
24
+ <Avatar initials="JM" />
25
+ <Avatar tone="indigo" initials="N" />
26
+ <Avatar tone="moss" initials="M" />
27
+ <Avatar tone="terra" initials="A" />
28
+ <Avatar tone="slate" initials="S" />
29
+ </div>
30
+ ),
31
+ }
32
+
33
+ export const Sizes: Story = {
34
+ parameters: { controls: { hideNoControlsWarning: true } },
35
+ render: () => (
36
+ <div className="flex items-end gap-3">
37
+ <Avatar size="sm" initials="SM" />
38
+ <Avatar size="md" initials="MD" />
39
+ <Avatar size="lg" initials="LG" />
40
+ </div>
41
+ ),
42
+ }
@@ -0,0 +1,48 @@
1
+ import type { HTMLAttributes } from 'react'
2
+ import { tv, type VariantProps } from 'tailwind-variants'
3
+
4
+ import { cn } from '../../lib/cn'
5
+
6
+ /* =========================================================================
7
+ Avatar — round, paper-tinted initials chip. Mono glyph, hairline border.
8
+ For square colored "app tiles" use the AppSwitcher's internal tile —
9
+ Avatar is reserved for people.
10
+ ========================================================================= */
11
+
12
+ export const avatarStyles = tv({
13
+ base: [
14
+ 'inline-grid place-items-center shrink-0 select-none',
15
+ 'rounded-full border',
16
+ 'font-mono font-medium tracking-[-0.005em]',
17
+ ],
18
+ variants: {
19
+ size: {
20
+ sm: 'size-[22px] text-[10px]',
21
+ md: 'size-7 text-[12px]',
22
+ lg: 'size-9 text-[13px]',
23
+ },
24
+ tone: {
25
+ paper: 'bg-surface-3 text-ink border-rule',
26
+ indigo: 'bg-accent text-[#f0efe8] border-transparent',
27
+ moss: 'bg-[#5b7052] text-[#f0efe8] border-transparent',
28
+ terra: 'bg-[#8a5a3f] text-[#f0efe8] border-transparent',
29
+ slate: 'bg-ink text-surface border-transparent',
30
+ },
31
+ },
32
+ defaultVariants: { size: 'md', tone: 'paper' },
33
+ })
34
+
35
+ export type AvatarVariants = VariantProps<typeof avatarStyles>
36
+ export type AvatarProps = HTMLAttributes<HTMLSpanElement> &
37
+ AvatarVariants & {
38
+ /** 1–2 character initials. Ignored if `children` is provided. */
39
+ initials?: string
40
+ }
41
+
42
+ export function Avatar({ size, tone, initials, className, children, ...props }: AvatarProps) {
43
+ return (
44
+ <span {...props} className={cn(avatarStyles({ size, tone }), className)}>
45
+ {children ?? initials}
46
+ </span>
47
+ )
48
+ }
@@ -0,0 +1,2 @@
1
+ export type { AvatarProps, AvatarVariants } from './avatar'
2
+ export { Avatar, avatarStyles } from './avatar'
@@ -0,0 +1,38 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { AvatarGroup } from './avatar-group'
5
+
6
+ describe('AvatarGroup', () => {
7
+ it('renders one avatar per person up to max', () => {
8
+ render(
9
+ <AvatarGroup
10
+ avatars={[
11
+ { initials: 'AB', key: 'a' },
12
+ { initials: 'CD', key: 'b' },
13
+ ]}
14
+ />,
15
+ )
16
+ expect(screen.getByText('AB')).toBeInTheDocument()
17
+ expect(screen.getByText('CD')).toBeInTheDocument()
18
+ expect(screen.queryByText(/^\+/)).not.toBeInTheDocument()
19
+ })
20
+
21
+ it('collapses the overflow into a +N chip', () => {
22
+ render(
23
+ <AvatarGroup
24
+ max={2}
25
+ avatars={[
26
+ { initials: 'AB', key: 'a' },
27
+ { initials: 'CD', key: 'b' },
28
+ { initials: 'EF', key: 'c' },
29
+ { initials: 'GH', key: 'd' },
30
+ ]}
31
+ />,
32
+ )
33
+ expect(screen.getByText('AB')).toBeInTheDocument()
34
+ expect(screen.getByText('CD')).toBeInTheDocument()
35
+ expect(screen.queryByText('EF')).not.toBeInTheDocument()
36
+ expect(screen.getByText('+2')).toBeInTheDocument()
37
+ })
38
+ })
@@ -0,0 +1,60 @@
1
+ import type { HTMLAttributes } from 'react'
2
+ import { tv, type VariantProps } from 'tailwind-variants'
3
+
4
+ import { cn } from '../../lib/cn'
5
+ import { Avatar, type AvatarVariants } from '../avatar/avatar'
6
+
7
+ /* =========================================================================
8
+ AvatarGroup — a stack of overlapping people avatars with a +N overflow
9
+ chip. Used to render a conversation's roster: one avatar for a 1:1 thread,
10
+ several overlapping for a group. Each avatar wears a bg-colored ring so the
11
+ overlap reads as separate chips.
12
+ ========================================================================= */
13
+
14
+ export const avatarGroupStyles = tv({
15
+ base: 'flex items-center',
16
+ variants: {
17
+ size: {
18
+ sm: '-space-x-1.5',
19
+ md: '-space-x-2',
20
+ lg: '-space-x-2.5',
21
+ },
22
+ },
23
+ defaultVariants: { size: 'md' },
24
+ })
25
+
26
+ export type AvatarGroupVariants = VariantProps<typeof avatarGroupStyles>
27
+
28
+ /** One person in the stack — initials plus an optional tone. */
29
+ export interface AvatarGroupItem {
30
+ initials: string
31
+ tone?: AvatarVariants['tone']
32
+ /** Stable key (e.g. the participant's external id); falls back to the index. */
33
+ key?: string
34
+ }
35
+
36
+ export type AvatarGroupProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children'> &
37
+ AvatarGroupVariants & {
38
+ avatars: AvatarGroupItem[]
39
+ /** Max avatars shown before the rest collapse into a +N chip. Default 3. */
40
+ max?: number
41
+ }
42
+
43
+ export function AvatarGroup({ avatars, max = 3, size = 'md', className, ...props }: AvatarGroupProps) {
44
+ const shown = avatars.slice(0, max)
45
+ const overflow = avatars.length - shown.length
46
+ return (
47
+ <span {...props} className={cn(avatarGroupStyles({ size }), className)}>
48
+ {shown.map((avatar, index) => (
49
+ <span key={avatar.key ?? index} className="rounded-full ring-[1.5px] ring-surface">
50
+ <Avatar size={size} tone={avatar.tone ?? 'paper'} initials={avatar.initials} />
51
+ </span>
52
+ ))}
53
+ {overflow > 0 && (
54
+ <span className="rounded-full ring-[1.5px] ring-surface">
55
+ <Avatar size={size} tone="paper" initials={`+${overflow}`} />
56
+ </span>
57
+ )}
58
+ </span>
59
+ )
60
+ }
@@ -0,0 +1,2 @@
1
+ export type { AvatarGroupItem, AvatarGroupProps, AvatarGroupVariants } from './avatar-group'
2
+ export { AvatarGroup, avatarGroupStyles } from './avatar-group'
@@ -0,0 +1,62 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+
3
+ import { Badge } from './badge'
4
+
5
+ const meta: Meta<typeof Badge> = {
6
+ title: 'Components/Badge',
7
+ component: Badge,
8
+ tags: ['autodocs'],
9
+ args: {
10
+ children: 'in transit',
11
+ variant: 'active',
12
+ },
13
+ argTypes: {
14
+ variant: {
15
+ control: { type: 'inline-radio' },
16
+ options: ['default', 'neutral', 'active', 'success', 'warning', 'danger'],
17
+ },
18
+ },
19
+ }
20
+
21
+ export default meta
22
+ type Story = StoryObj<typeof Badge>
23
+
24
+ export const Playground: Story = {}
25
+
26
+ export const AllVariants: Story = {
27
+ parameters: { controls: { hideNoControlsWarning: true } },
28
+ render: () => (
29
+ <div className="flex flex-wrap items-center gap-2">
30
+ <Badge variant="active">in transit</Badge>
31
+ <Badge>quoted</Badge>
32
+ <Badge variant="neutral">dispatching</Badge>
33
+ <Badge variant="success">reconciled</Badge>
34
+ <Badge variant="warning">past due</Badge>
35
+ <Badge variant="danger">exception</Badge>
36
+ </div>
37
+ ),
38
+ }
39
+
40
+ export const InContext: Story = {
41
+ parameters: { controls: { hideNoControlsWarning: true } },
42
+ render: () => (
43
+ <div className="w-[480px] grid gap-3 font-mono text-[12.5px] text-ink-2">
44
+ <div className="flex items-center justify-between border-b border-dashed border-rule pb-3">
45
+ <span>SH-10482 · DAL → ATL</span>
46
+ <Badge variant="active">in transit</Badge>
47
+ </div>
48
+ <div className="flex items-center justify-between border-b border-dashed border-rule pb-3">
49
+ <span>SH-10483 · CHI → MSP</span>
50
+ <Badge variant="neutral">dispatching</Badge>
51
+ </div>
52
+ <div className="flex items-center justify-between border-b border-dashed border-rule pb-3">
53
+ <span>SH-10486 · MIA → JFK</span>
54
+ <Badge variant="warning">past due</Badge>
55
+ </div>
56
+ <div className="flex items-center justify-between">
57
+ <span>SH-10487 · SEA → BOI</span>
58
+ <Badge variant="danger">exception</Badge>
59
+ </div>
60
+ </div>
61
+ ),
62
+ }
@@ -0,0 +1,36 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { Badge } from './badge'
5
+
6
+ describe('Badge', () => {
7
+ it('renders its content', () => {
8
+ render(<Badge>in transit</Badge>)
9
+ expect(screen.getByText('in transit')).toBeInTheDocument()
10
+ })
11
+
12
+ it('applies the default variant classes', () => {
13
+ render(<Badge>quoted</Badge>)
14
+ expect(screen.getByText('quoted').className).toContain('text-ink-3')
15
+ })
16
+
17
+ it('applies the active variant classes', () => {
18
+ render(<Badge variant="active">in transit</Badge>)
19
+ expect(screen.getByText('in transit').className).toContain('text-accent-ink')
20
+ })
21
+
22
+ it('applies the danger variant classes', () => {
23
+ render(<Badge variant="danger">exception</Badge>)
24
+ expect(screen.getByText('exception').className).toContain('text-danger')
25
+ })
26
+
27
+ it('merges custom classes', () => {
28
+ render(
29
+ <Badge variant="success" className="ml-3">
30
+ reconciled
31
+ </Badge>,
32
+ )
33
+ expect(screen.getByText('reconciled').className).toContain('ml-3')
34
+ expect(screen.getByText('reconciled').className).toContain('text-success')
35
+ })
36
+ })
@@ -0,0 +1,54 @@
1
+ import type { HTMLAttributes } from 'react'
2
+ import { tv, type VariantProps } from 'tailwind-variants'
3
+
4
+ import { cn } from '../../lib/cn'
5
+
6
+ /* =========================================================================
7
+ Badge — mono, pill-shaped status chip with a leading dot.
8
+ Matches the Proteos spec's six registers: default, neutral, active (indigo),
9
+ success, warning, danger. Paper-compatible fills — never pure-color.
10
+ ========================================================================= */
11
+
12
+ export const badgeStyles = tv({
13
+ base: [
14
+ 'inline-flex items-center gap-1.5',
15
+ 'font-mono text-[11.5px] tracking-[-0.005em]',
16
+ 'px-2.5 py-0.5 rounded-pill border',
17
+ // Leading dot
18
+ 'before:content-[""] before:block before:size-1.5 before:rounded-full',
19
+ ],
20
+ variants: {
21
+ variant: {
22
+ default: 'border-rule bg-surface text-ink-3 before:bg-ink-4',
23
+ neutral: 'border-rule bg-surface-3 text-ink-2 before:bg-ink-3',
24
+ active: [
25
+ 'text-accent-ink before:bg-accent',
26
+ 'border-[color-mix(in_oklab,var(--color-accent)_35%,var(--color-rule))]',
27
+ 'bg-[color-mix(in_oklab,var(--color-accent)_14%,var(--color-surface))]',
28
+ ],
29
+ success: [
30
+ 'text-success before:bg-success',
31
+ 'border-[color-mix(in_oklab,var(--color-success)_35%,var(--color-rule))]',
32
+ 'bg-[color-mix(in_oklab,var(--color-success)_12%,var(--color-surface))]',
33
+ ],
34
+ warning: [
35
+ 'text-warning before:bg-warning',
36
+ 'border-[color-mix(in_oklab,var(--color-warning)_35%,var(--color-rule))]',
37
+ 'bg-[color-mix(in_oklab,var(--color-warning)_14%,var(--color-surface))]',
38
+ ],
39
+ danger: [
40
+ 'text-danger before:bg-danger',
41
+ 'border-[color-mix(in_oklab,var(--color-danger)_40%,var(--color-rule))]',
42
+ 'bg-[color-mix(in_oklab,var(--color-danger)_12%,var(--color-surface))]',
43
+ ],
44
+ },
45
+ },
46
+ defaultVariants: { variant: 'default' },
47
+ })
48
+
49
+ export type BadgeVariants = VariantProps<typeof badgeStyles>
50
+ export type BadgeProps = HTMLAttributes<HTMLSpanElement> & BadgeVariants
51
+
52
+ export function Badge({ variant, className, ...props }: BadgeProps) {
53
+ return <span {...props} className={cn(badgeStyles({ variant }), className)} />
54
+ }
@@ -0,0 +1,2 @@
1
+ export type { BadgeProps, BadgeVariants } from './badge'
2
+ export { Badge, badgeStyles } from './badge'
@@ -0,0 +1,177 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useState } from 'react'
3
+
4
+ import { BadgeMultiSelect, type BadgeMultiSelectOption } from './badge-multi-select'
5
+
6
+ const meta: Meta<typeof BadgeMultiSelect> = {
7
+ title: 'Components/BadgeMultiSelect',
8
+ component: BadgeMultiSelect,
9
+ tags: ['autodocs'],
10
+ decorators: [
11
+ (Story) => (
12
+ <div className="w-[440px]">
13
+ <Story />
14
+ </div>
15
+ ),
16
+ ],
17
+ }
18
+
19
+ export default meta
20
+ type Story = StoryObj<typeof BadgeMultiSelect>
21
+
22
+ const statuses: BadgeMultiSelectOption<string>[] = [
23
+ { id: 'in-transit', label: 'in transit', variant: 'active' },
24
+ { id: 'quoted', label: 'quoted' },
25
+ { id: 'dispatching', label: 'dispatching', variant: 'neutral' },
26
+ { id: 'reconciled', label: 'reconciled', variant: 'success' },
27
+ { id: 'past-due', label: 'past due', variant: 'warning' },
28
+ { id: 'exception', label: 'exception', variant: 'danger' },
29
+ ]
30
+
31
+ export const Playground: Story = {
32
+ render: () => {
33
+ const [selected, setSelected] = useState<Set<string>>(new Set(['in-transit', 'past-due']))
34
+ return (
35
+ <BadgeMultiSelect
36
+ label="Statuses to include"
37
+ placeholder="Any status"
38
+ description="Filter the shipment board by one or more statuses."
39
+ options={statuses}
40
+ selectedKeys={selected}
41
+ onSelectionChange={setSelected}
42
+ />
43
+ )
44
+ },
45
+ }
46
+
47
+ export const Empty: Story = {
48
+ render: () => {
49
+ const [selected, setSelected] = useState<Set<string>>(new Set())
50
+ return (
51
+ <BadgeMultiSelect
52
+ label="Statuses"
53
+ placeholder="Any status"
54
+ options={statuses}
55
+ selectedKeys={selected}
56
+ onSelectionChange={setSelected}
57
+ />
58
+ )
59
+ },
60
+ }
61
+
62
+ /** `hideSelectedOptions` — already-selected items disappear from the
63
+ * dropdown. Removal happens only via the chip × in the trigger. Useful
64
+ * when the list is long and you want to see what's left to pick. */
65
+ export const HideSelected: Story = {
66
+ render: () => {
67
+ const [selected, setSelected] = useState<Set<string>>(new Set(['in-transit']))
68
+ return (
69
+ <BadgeMultiSelect
70
+ label="Statuses"
71
+ placeholder="Any status"
72
+ description="Selected statuses are hidden from the menu — remove them via × above."
73
+ options={statuses}
74
+ selectedKeys={selected}
75
+ onSelectionChange={setSelected}
76
+ hideSelectedOptions
77
+ />
78
+ )
79
+ },
80
+ }
81
+
82
+ export const ManySelected: Story = {
83
+ render: () => {
84
+ const [selected, setSelected] = useState<Set<string>>(
85
+ new Set(['in-transit', 'quoted', 'reconciled', 'past-due', 'exception']),
86
+ )
87
+ return (
88
+ <BadgeMultiSelect
89
+ label="Statuses"
90
+ placeholder="Any status"
91
+ options={statuses}
92
+ selectedKeys={selected}
93
+ onSelectionChange={setSelected}
94
+ />
95
+ )
96
+ },
97
+ }
98
+
99
+ export const Invalid: Story = {
100
+ render: () => {
101
+ const [selected, setSelected] = useState<Set<string>>(new Set())
102
+ return (
103
+ <BadgeMultiSelect
104
+ label="Statuses"
105
+ placeholder="Any status"
106
+ options={statuses}
107
+ selectedKeys={selected}
108
+ onSelectionChange={setSelected}
109
+ isInvalid
110
+ errorMessage="Pick at least one status."
111
+ />
112
+ )
113
+ },
114
+ }
115
+
116
+ export const Disabled: Story = {
117
+ render: () => (
118
+ <BadgeMultiSelect
119
+ label="Statuses"
120
+ options={statuses}
121
+ selectedKeys={new Set(['in-transit', 'past-due'])}
122
+ onSelectionChange={() => {}}
123
+ isDisabled
124
+ />
125
+ ),
126
+ }
127
+
128
+ export const AllStates: Story = {
129
+ parameters: { controls: { hideNoControlsWarning: true } },
130
+ render: () => {
131
+ const [a, setA] = useState<Set<string>>(new Set())
132
+ const [b, setB] = useState<Set<string>>(new Set(['in-transit']))
133
+ const [c, setC] = useState<Set<string>>(new Set(['in-transit', 'past-due', 'exception']))
134
+ return (
135
+ <div className="grid gap-5 w-[440px]">
136
+ <BadgeMultiSelect
137
+ label="Empty"
138
+ placeholder="Any status"
139
+ options={statuses}
140
+ selectedKeys={a}
141
+ onSelectionChange={setA}
142
+ />
143
+ <BadgeMultiSelect
144
+ label="One selection"
145
+ placeholder="Any status"
146
+ options={statuses}
147
+ selectedKeys={b}
148
+ onSelectionChange={setB}
149
+ />
150
+ <BadgeMultiSelect
151
+ label="Multiple selections"
152
+ placeholder="Any status"
153
+ options={statuses}
154
+ selectedKeys={c}
155
+ onSelectionChange={setC}
156
+ />
157
+ <BadgeMultiSelect
158
+ label="Invalid"
159
+ placeholder="Any status"
160
+ options={statuses}
161
+ selectedKeys={new Set<string>()}
162
+ onSelectionChange={() => {}}
163
+ isInvalid
164
+ errorMessage="Pick at least one status."
165
+ />
166
+ <BadgeMultiSelect
167
+ label="Disabled"
168
+ placeholder="Any status"
169
+ options={statuses}
170
+ selectedKeys={new Set(['in-transit'])}
171
+ onSelectionChange={() => {}}
172
+ isDisabled
173
+ />
174
+ </div>
175
+ )
176
+ },
177
+ }