@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,7 @@
1
+ export type {
2
+ NavGroupProps,
3
+ NavItemProps,
4
+ NavItemVariants,
5
+ NavSectionProps,
6
+ } from './nav'
7
+ export { NavGroup, NavItem, NavSection, navItemStyles } from './nav'
@@ -0,0 +1,139 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+
3
+ import { NavGroup, NavItem, NavSection } from './nav'
4
+
5
+ /* =========================================================================
6
+ Nav primitives — render inside a sidebar-shaped frame to show realistic
7
+ density (256px column, paper bg, hairline border).
8
+ ========================================================================= */
9
+
10
+ const meta: Meta<typeof NavSection> = {
11
+ title: 'Components/Nav',
12
+ component: NavSection,
13
+ tags: ['autodocs'],
14
+ decorators: [
15
+ (Story) => (
16
+ <div className="w-[256px] border border-rule bg-[color-mix(in_oklab,var(--color-surface-2)_50%,var(--color-surface))] p-2.5">
17
+ <Story />
18
+ </div>
19
+ ),
20
+ ],
21
+ }
22
+
23
+ export default meta
24
+ type Story = StoryObj<typeof NavSection>
25
+
26
+ const HomeIcon = (
27
+ <svg viewBox="0 0 16 16" aria-hidden>
28
+ <path
29
+ d="M2 7.5 8 2.5l6 5V13a1 1 0 0 1-1 1h-3v-4H7v4H4a1 1 0 0 1-1-1V7.5h-1Z"
30
+ fill="none"
31
+ stroke="currentColor"
32
+ strokeWidth="1.25"
33
+ strokeLinejoin="round"
34
+ />
35
+ </svg>
36
+ )
37
+
38
+ const TruckIcon = (
39
+ <svg viewBox="0 0 16 16" aria-hidden>
40
+ <path
41
+ d="M1.5 4h7v7h-7zM8.5 6h3l2 2v3h-5zM4 13.5a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Zm7 0a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z"
42
+ fill="none"
43
+ stroke="currentColor"
44
+ strokeWidth="1.2"
45
+ strokeLinejoin="round"
46
+ />
47
+ </svg>
48
+ )
49
+
50
+ const UsersIcon = (
51
+ <svg viewBox="0 0 16 16" aria-hidden>
52
+ <circle cx="6" cy="5.5" r="2.25" fill="none" stroke="currentColor" strokeWidth="1.2" />
53
+ <path
54
+ d="M2 13c.5-2 2-3.25 4-3.25S9.5 11 10 13"
55
+ fill="none"
56
+ stroke="currentColor"
57
+ strokeWidth="1.2"
58
+ strokeLinecap="round"
59
+ />
60
+ </svg>
61
+ )
62
+
63
+ export const Playground: Story = {
64
+ parameters: { controls: { hideNoControlsWarning: true } },
65
+ render: () => (
66
+ <NavSection label="Workspace" count={3}>
67
+ <NavItem icon={HomeIcon}>Home</NavItem>
68
+ <NavItem icon={UsersIcon} badge="418">
69
+ Customers
70
+ </NavItem>
71
+ <NavItem icon={TruckIcon} isActive>
72
+ Shipments
73
+ </NavItem>
74
+ </NavSection>
75
+ ),
76
+ }
77
+
78
+ export const ItemStates: Story = {
79
+ parameters: { controls: { hideNoControlsWarning: true } },
80
+ render: () => (
81
+ <NavSection label="States">
82
+ <NavItem icon={HomeIcon}>Default</NavItem>
83
+ <NavItem icon={HomeIcon} isActive>
84
+ Active
85
+ </NavItem>
86
+ <NavItem icon={HomeIcon} badge="12">
87
+ With badge
88
+ </NavItem>
89
+ <NavItem icon={HomeIcon} isDisabled>
90
+ Disabled
91
+ </NavItem>
92
+ </NavSection>
93
+ ),
94
+ }
95
+
96
+ export const WithGroup: Story = {
97
+ parameters: { controls: { hideNoControlsWarning: true } },
98
+ render: () => (
99
+ <NavSection label="Operations" count={5}>
100
+ <NavItem icon={UsersIcon} badge="418">
101
+ Customers
102
+ </NavItem>
103
+ <NavGroup icon={TruckIcon} label="Shipments" defaultExpanded>
104
+ <NavItem isActive badge="1,284">
105
+ Open
106
+ </NavItem>
107
+ <NavItem badge="37">Dispatching</NavItem>
108
+ <NavItem badge="208">In transit</NavItem>
109
+ <NavItem>Delivered</NavItem>
110
+ <NavItem>Reconciled</NavItem>
111
+ </NavGroup>
112
+ <NavGroup icon={UsersIcon} label="Drivers">
113
+ <NavItem>Roster</NavItem>
114
+ <NavItem>Schedules</NavItem>
115
+ <NavItem badge="3">Compliance</NavItem>
116
+ </NavGroup>
117
+ </NavSection>
118
+ ),
119
+ }
120
+
121
+ export const MultipleSections: Story = {
122
+ parameters: { controls: { hideNoControlsWarning: true } },
123
+ render: () => (
124
+ <>
125
+ <NavSection label="Workspace">
126
+ <NavItem icon={HomeIcon}>Home</NavItem>
127
+ <NavItem icon={HomeIcon} badge="12">
128
+ Inbox
129
+ </NavItem>
130
+ </NavSection>
131
+ <NavSection label="Operations" count={2}>
132
+ <NavItem icon={UsersIcon}>Customers</NavItem>
133
+ <NavItem icon={TruckIcon} isActive>
134
+ Shipments
135
+ </NavItem>
136
+ </NavSection>
137
+ </>
138
+ ),
139
+ }
@@ -0,0 +1,246 @@
1
+ import { createContext, type ReactNode, useContext } from 'react'
2
+ import {
3
+ Button as AriaButton,
4
+ type ButtonProps as AriaButtonProps,
5
+ type DisclosureProps as AriaDisclosureProps,
6
+ Disclosure,
7
+ DisclosurePanel,
8
+ Heading,
9
+ } from 'react-aria-components'
10
+ import { tv, type VariantProps } from 'tailwind-variants'
11
+
12
+ import { cn } from '../../lib/cn'
13
+
14
+ /* =========================================================================
15
+ Nav — vertical navigation primitives for sidebars / drawers. Three pieces:
16
+ - <NavSection> — labelled grouping with optional count + trailing slot.
17
+ - <NavItem> — leaf entry (RAC Button) with icon, label, badge slot,
18
+ and an `isActive` state (raised paper card + accent rail).
19
+ - <NavGroup> — collapsible parent (RAC Disclosure + DisclosurePanel)
20
+ that auto-tags its children as nested via context.
21
+
22
+ The primitives don't know about "collapsed sidebar" — that visual cascade
23
+ lives in the consuming app, which can hide labels / center icons via CSS
24
+ on a wrapping element.
25
+ ========================================================================= */
26
+
27
+ /* ---------- nested context ----------------------------------------------- */
28
+
29
+ const NavNestedContext = createContext(false)
30
+ /** True when a NavItem is rendered inside a NavGroup's panel. */
31
+ function useIsNested() {
32
+ return useContext(NavNestedContext)
33
+ }
34
+
35
+ /* ---------- NavSection --------------------------------------------------- */
36
+
37
+ export type NavSectionProps = {
38
+ /** Uppercase mono label rendered above the items. */
39
+ label?: ReactNode
40
+ /** Counter rendered at the right of the label (e.g. `5`). */
41
+ count?: ReactNode
42
+ /** Trailing slot at the right of the heading row, e.g. an action button. */
43
+ action?: ReactNode
44
+ className?: string
45
+ children?: ReactNode
46
+ /** Stable test/automation id, forwarded to the section root. */
47
+ 'data-testid'?: string
48
+ }
49
+
50
+ /** Visually-grouped block of nav entries. Use one per logical section
51
+ * ("Workspace", "Operations", "Finance"). */
52
+ export function NavSection({
53
+ label,
54
+ count,
55
+ action,
56
+ className,
57
+ children,
58
+ 'data-testid': dataTestid,
59
+ }: NavSectionProps) {
60
+ const hasHead = label !== undefined || count !== undefined || action !== undefined
61
+ return (
62
+ <div className={cn('mt-[18px] first:mt-1', className)} data-testid={dataTestid}>
63
+ {hasHead ? (
64
+ <div className="flex items-center justify-between px-2 pt-1 pb-2">
65
+ {label !== undefined ? (
66
+ <span className="font-mono text-[10.5px] uppercase tracking-[0.08em] text-ink-4">
67
+ {label}
68
+ </span>
69
+ ) : (
70
+ <span />
71
+ )}
72
+ <div className="flex items-center gap-2">
73
+ {count !== undefined ? (
74
+ <span className="font-mono text-[10px] text-ink-4">{count}</span>
75
+ ) : null}
76
+ {action}
77
+ </div>
78
+ </div>
79
+ ) : null}
80
+ {children}
81
+ </div>
82
+ )
83
+ }
84
+
85
+ /* ---------- NavItem ------------------------------------------------------ */
86
+
87
+ export const navItemStyles = tv({
88
+ base: [
89
+ 'group/nav-item relative w-full',
90
+ 'flex items-center gap-2.5',
91
+ 'px-2 py-[7px] rounded-[7px]',
92
+ 'text-[13.5px] tracking-[-0.005em] text-ink-2',
93
+ 'cursor-pointer outline-none text-left',
94
+ 'transition-[background-color,color,border-color,box-shadow] duration-150',
95
+ 'border border-transparent',
96
+ 'hover:bg-surface-2 hover:text-ink',
97
+ 'focus-visible:shadow-[var(--shadow-focus)]',
98
+ 'disabled:opacity-45 disabled:cursor-not-allowed disabled:pointer-events-none',
99
+ ],
100
+ variants: {
101
+ isActive: {
102
+ // Match the hover state's fill (bg-surface-2 + ink) so the selected color
103
+ // doesn't read brighter than the surrounding chrome. The hairline
104
+ // border + accent rail still mark the current page.
105
+ true: [
106
+ 'bg-surface-2 text-ink border-rule',
107
+ // accent rail anchored to the section's left edge
108
+ 'before:content-[""] before:absolute before:-left-2.5 before:top-2 before:bottom-2',
109
+ 'before:w-0.5 before:rounded-[2px] before:bg-accent',
110
+ ],
111
+ false: '',
112
+ },
113
+ isNested: {
114
+ true: 'text-[13px] text-ink-3 py-1.5',
115
+ false: '',
116
+ },
117
+ },
118
+ compoundVariants: [
119
+ // Nested + active: raise to ink rather than accent-ink so the indented
120
+ // hierarchy still reads "softer than top-level".
121
+ { isNested: true, isActive: true, class: 'text-ink' },
122
+ ],
123
+ defaultVariants: { isActive: false, isNested: false },
124
+ })
125
+
126
+ export type NavItemVariants = VariantProps<typeof navItemStyles>
127
+
128
+ export type NavItemProps = Omit<AriaButtonProps, 'children'> & {
129
+ /** Leading icon. Pass an SVG element; it's wrapped in a 16px box. */
130
+ icon?: ReactNode
131
+ /** Trailing slot — typically a count or status dot. */
132
+ badge?: ReactNode
133
+ /** Mark the item as the current page. */
134
+ isActive?: boolean
135
+ children?: ReactNode
136
+ }
137
+
138
+ /** Leaf nav entry. RAC Button under the hood — full keyboard, focus-visible
139
+ * ring, and disabled state included. For routing, apply `navItemStyles({...})`
140
+ * to a RAC `<Link>` instead. */
141
+ export function NavItem({ icon, badge, isActive, className, children, ...props }: NavItemProps) {
142
+ const isNested = useIsNested()
143
+ return (
144
+ <AriaButton
145
+ {...props}
146
+ className={(renderProps) =>
147
+ cn(
148
+ navItemStyles({ isActive, isNested }),
149
+ typeof className === 'function' ? className(renderProps) : className,
150
+ )
151
+ }
152
+ >
153
+ {icon !== undefined ? (
154
+ <span
155
+ aria-hidden
156
+ className={cn(
157
+ 'grid place-items-center shrink-0 size-4',
158
+ isActive ? 'text-accent' : 'text-ink-3',
159
+ '[&>svg]:size-4',
160
+ )}
161
+ >
162
+ {icon}
163
+ </span>
164
+ ) : null}
165
+ <span className="flex-1 min-w-0 truncate">{children}</span>
166
+ {badge !== undefined ? (
167
+ <span className="shrink-0 font-mono text-[10.5px] tracking-[0.02em] text-ink-4">
168
+ {badge}
169
+ </span>
170
+ ) : null}
171
+ </AriaButton>
172
+ )
173
+ }
174
+
175
+ /* ---------- NavGroup ----------------------------------------------------- */
176
+
177
+ export type NavGroupProps = Omit<AriaDisclosureProps, 'children'> & {
178
+ /** Leading icon for the group head. */
179
+ icon?: ReactNode
180
+ /** Group label rendered next to the icon. */
181
+ label: ReactNode
182
+ /** Children get auto-styled as nested NavItems via context. */
183
+ children: ReactNode
184
+ className?: string
185
+ }
186
+
187
+ /** Collapsible parent. RAC Disclosure handles aria-expanded / aria-controls
188
+ * and keyboard activation. Children render inside an indented panel with a
189
+ * vertical guide rule and pick up nested styling automatically. */
190
+ export function NavGroup({ icon, label, children, className, ...props }: NavGroupProps) {
191
+ return (
192
+ <Disclosure {...props} className={cn('group/nav-group my-px', className)}>
193
+ <Heading className="m-0">
194
+ <AriaButton
195
+ slot="trigger"
196
+ className={cn(
197
+ 'flex w-full items-center gap-2.5',
198
+ 'px-2 py-[7px] rounded-[7px]',
199
+ 'text-[13.5px] tracking-[-0.005em] text-ink-2 text-left',
200
+ 'cursor-pointer outline-none border border-transparent',
201
+ 'transition-colors duration-150',
202
+ 'hover:bg-surface-2 hover:text-ink',
203
+ 'focus-visible:shadow-[var(--shadow-focus)]',
204
+ )}
205
+ >
206
+ {icon !== undefined ? (
207
+ <span
208
+ aria-hidden
209
+ className="grid place-items-center shrink-0 size-4 text-ink-3 [&>svg]:size-4"
210
+ >
211
+ {icon}
212
+ </span>
213
+ ) : null}
214
+ <span className="flex-1 min-w-0 truncate">{label}</span>
215
+ <svg
216
+ viewBox="0 0 12 12"
217
+ aria-hidden
218
+ className="size-2.5 shrink-0 text-ink-4 transition-transform duration-200 group-data-[expanded]/nav-group:rotate-90"
219
+ >
220
+ <path
221
+ d="m4.5 3 3 3-3 3"
222
+ fill="none"
223
+ stroke="currentColor"
224
+ strokeWidth="1.4"
225
+ strokeLinecap="round"
226
+ strokeLinejoin="round"
227
+ />
228
+ </svg>
229
+ </AriaButton>
230
+ </Heading>
231
+ <DisclosurePanel>
232
+ <div
233
+ className={cn(
234
+ 'relative pl-[22px] py-px',
235
+ // Vertical guide rule — sits under where the parent's icon center
236
+ // would be (14px from the panel's left edge).
237
+ 'before:content-[""] before:absolute before:left-[14px] before:top-1 before:bottom-1',
238
+ 'before:w-px before:bg-rule',
239
+ )}
240
+ >
241
+ <NavNestedContext.Provider value={true}>{children}</NavNestedContext.Provider>
242
+ </div>
243
+ </DisclosurePanel>
244
+ </Disclosure>
245
+ )
246
+ }
@@ -0,0 +1,8 @@
1
+ export type { PopoverContentProps } from './popover'
2
+ export {
3
+ Popover,
4
+ PopoverContent,
5
+ PopoverPanelBody,
6
+ PopoverPanelFooter,
7
+ PopoverPanelHeader,
8
+ } from './popover'
@@ -0,0 +1,92 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useRef, useState } from 'react'
3
+
4
+ import { Button } from '../button/button'
5
+ import {
6
+ Popover,
7
+ PopoverContent,
8
+ PopoverPanelBody,
9
+ PopoverPanelFooter,
10
+ PopoverPanelHeader,
11
+ } from './popover'
12
+
13
+ const meta: Meta = {
14
+ title: 'Components/Popover',
15
+ decorators: [
16
+ (Story) => (
17
+ <div className="p-12">
18
+ <Story />
19
+ </div>
20
+ ),
21
+ ],
22
+ }
23
+
24
+ export default meta
25
+ type Story = StoryObj
26
+
27
+ export const DialogTriggerMode: Story = {
28
+ render: () => (
29
+ <Popover>
30
+ <Button variant="ghost">Open menu</Button>
31
+ <PopoverContent panelClassName="w-72">
32
+ <PopoverPanelHeader>
33
+ <span className="font-mono text-[10.5px] uppercase tracking-[0.08em] text-ink-4">
34
+ Quick actions
35
+ </span>
36
+ </PopoverPanelHeader>
37
+ <PopoverPanelBody>
38
+ <div className="grid gap-1.5 text-[13px] text-ink-2">
39
+ <button
40
+ type="button"
41
+ className="rounded-sm px-2.5 py-1.5 text-left hover:bg-surface-2 cursor-pointer"
42
+ >
43
+ Edit profile
44
+ </button>
45
+ <button
46
+ type="button"
47
+ className="rounded-sm px-2.5 py-1.5 text-left hover:bg-surface-2 cursor-pointer"
48
+ >
49
+ Manage workspace
50
+ </button>
51
+ <button
52
+ type="button"
53
+ className="rounded-sm px-2.5 py-1.5 text-left hover:bg-surface-2 cursor-pointer"
54
+ >
55
+ Sign out
56
+ </button>
57
+ </div>
58
+ </PopoverPanelBody>
59
+ <PopoverPanelFooter>
60
+ <span className="font-mono text-[11px] text-ink-4">⌘K</span>
61
+ </PopoverPanelFooter>
62
+ </PopoverContent>
63
+ </Popover>
64
+ ),
65
+ }
66
+
67
+ export const ControlledMode: Story = {
68
+ render: () => {
69
+ const ref = useRef<HTMLButtonElement>(null)
70
+ const [open, setOpen] = useState(false)
71
+ return (
72
+ <>
73
+ <button
74
+ ref={ref}
75
+ type="button"
76
+ onClick={() => setOpen((v) => !v)}
77
+ className="inline-flex items-center gap-2 rounded-md border border-rule bg-surface px-3 py-2 text-[13px] text-ink-2 cursor-pointer hover:border-ink-4"
78
+ >
79
+ Custom trigger (chip-style)
80
+ </button>
81
+ <PopoverContent triggerRef={ref} isOpen={open} onOpenChange={setOpen} panelClassName="w-72">
82
+ <PopoverPanelBody>
83
+ <p className="text-[13px] text-ink-2">
84
+ This popover is anchored to a non-Button trigger via
85
+ <code className="font-mono text-[12px] mx-1">triggerRef</code>.
86
+ </p>
87
+ </PopoverPanelBody>
88
+ </PopoverContent>
89
+ </>
90
+ )
91
+ },
92
+ }
@@ -0,0 +1,135 @@
1
+ import type { ReactNode, RefObject } from 'react'
2
+ import {
3
+ Dialog as AriaDialog,
4
+ type DialogProps as AriaDialogProps,
5
+ DialogTrigger as AriaDialogTrigger,
6
+ type DialogTriggerProps as AriaDialogTriggerProps,
7
+ Popover as AriaPopover,
8
+ type PopoverProps as AriaPopoverProps,
9
+ } from 'react-aria-components'
10
+
11
+ import { cn } from '../../lib/cn'
12
+
13
+ /* =========================================================================
14
+ Popover — generic anchored panel. Two usage modes:
15
+
16
+ 1. DialogTrigger mode (simplest, for Button-driven popovers):
17
+ <Popover>
18
+ <Button>Open</Button> // first child = trigger
19
+ <PopoverContent>…panel…</PopoverContent>
20
+ </Popover>
21
+
22
+ 2. Controlled mode (for non-Button triggers, e.g. clickable chips):
23
+ const ref = useRef<HTMLSpanElement>(null)
24
+ const [open, setOpen] = useState(false)
25
+ …
26
+ <span ref={ref} onClick={…}>chip</span>
27
+ <PopoverContent
28
+ triggerRef={ref}
29
+ isOpen={open}
30
+ onOpenChange={setOpen}
31
+ >…panel…</PopoverContent>
32
+
33
+ Internally wraps RAC `Popover` + `Dialog` so focus management, outside-
34
+ click, and Escape are handled correctly.
35
+ ========================================================================= */
36
+
37
+ export function Popover(props: AriaDialogTriggerProps) {
38
+ return <AriaDialogTrigger {...props} />
39
+ }
40
+
41
+ export type PopoverContentProps = Omit<AriaPopoverProps, 'children'> & {
42
+ /** Dialog children. Supports the render-prop form `({ close }) => …`. */
43
+ children: AriaDialogProps['children']
44
+ /** Visual override for the floating panel. */
45
+ panelClassName?: string
46
+ /** When in controlled mode, the element the popover should anchor to. */
47
+ triggerRef?: RefObject<Element | null>
48
+ }
49
+
50
+ export function PopoverContent({
51
+ children,
52
+ className,
53
+ panelClassName,
54
+ offset = 6,
55
+ placement = 'bottom start',
56
+ ...props
57
+ }: PopoverContentProps) {
58
+ return (
59
+ <AriaPopover
60
+ offset={offset}
61
+ placement={placement}
62
+ {...props}
63
+ className={(renderProps) =>
64
+ cn(
65
+ 'rounded-md border border-rule bg-surface',
66
+ 'shadow-[var(--shadow-float)]',
67
+ 'entering:animate-in entering:fade-in-0 entering:slide-in-from-top-1',
68
+ 'exiting:animate-out exiting:fade-out-0 exiting:slide-out-to-top-1',
69
+ panelClassName,
70
+ typeof className === 'function' ? className(renderProps) : className,
71
+ )
72
+ }
73
+ >
74
+ <AriaDialog className="outline-none">{children}</AriaDialog>
75
+ </AriaPopover>
76
+ )
77
+ }
78
+
79
+ /* =========================================================================
80
+ PopoverPanelHeader / PopoverPanelBody / PopoverPanelFooter — slot
81
+ primitives mirroring Modal's split. Consumers compose these to keep
82
+ padding consistent across popovers.
83
+ ========================================================================= */
84
+
85
+ export function PopoverPanelHeader({
86
+ className,
87
+ children,
88
+ }: {
89
+ className?: string
90
+ children?: ReactNode
91
+ }) {
92
+ return (
93
+ <div
94
+ className={cn(
95
+ 'flex items-center justify-between gap-2',
96
+ 'px-3.5 py-2.5 border-b border-rule',
97
+ className,
98
+ )}
99
+ >
100
+ {children}
101
+ </div>
102
+ )
103
+ }
104
+
105
+ export function PopoverPanelBody({
106
+ className,
107
+ children,
108
+ }: {
109
+ className?: string
110
+ children?: ReactNode
111
+ }) {
112
+ return <div className={cn('p-2', className)}>{children}</div>
113
+ }
114
+
115
+ export function PopoverPanelFooter({
116
+ className,
117
+ children,
118
+ }: {
119
+ className?: string
120
+ children?: ReactNode
121
+ }) {
122
+ return (
123
+ <div
124
+ className={cn(
125
+ 'flex items-center justify-between gap-2',
126
+ 'px-3.5 py-2.5 border-t border-rule',
127
+ 'bg-[color-mix(in_oklab,var(--color-surface-2)_60%,var(--color-surface))]',
128
+ 'rounded-b-md',
129
+ className,
130
+ )}
131
+ >
132
+ {children}
133
+ </div>
134
+ )
135
+ }
@@ -0,0 +1,2 @@
1
+ export type { ProgressBarProps, ProgressBarVariants } from './progress-bar'
2
+ export { ProgressBar, progressBarStyles } from './progress-bar'