@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,98 @@
1
+ import type { ReactNode } from 'react'
2
+ import {
3
+ TimeField as AriaTimeField,
4
+ type TimeFieldProps as AriaTimeFieldProps,
5
+ DateInput,
6
+ DateSegment,
7
+ FieldError,
8
+ Label,
9
+ Text,
10
+ type TimeValue,
11
+ } from 'react-aria-components'
12
+ import { cn } from '../../lib/cn'
13
+ import {
14
+ type FieldAppearance,
15
+ fieldControlStyles,
16
+ fieldErrorStyles,
17
+ type FieldSize,
18
+ fieldHintStyles,
19
+ fieldLabelStyles,
20
+ } from '../field/field'
21
+
22
+ export type TimeFieldProps<T extends TimeValue> = AriaTimeFieldProps<T> & {
23
+ label?: ReactNode
24
+ description?: ReactNode
25
+ errorMessage?: ReactNode
26
+ /** Field size. Defaults to `md`. */
27
+ size?: FieldSize
28
+ /** Chrome variant. Defaults to `field`. `cell` flattens the box. */
29
+ appearance?: FieldAppearance
30
+ }
31
+
32
+ /* Standalone time editor — backs the `time-picker` control (naive `HH:mm:ss`).
33
+ Just the segmented input: no calendar, no popover. Box chrome + content
34
+ padding both ride on the DateInput since there's no sibling trigger to share
35
+ the border with. */
36
+ export function TimeField<T extends TimeValue = TimeValue>({
37
+ label,
38
+ description,
39
+ errorMessage,
40
+ size,
41
+ appearance,
42
+ className,
43
+ ...props
44
+ }: TimeFieldProps<T>) {
45
+ const styles = fieldControlStyles({ size, appearance })
46
+ const focusWithin =
47
+ appearance === 'cell'
48
+ ? 'focus-within:border-accent focus-within:shadow-none'
49
+ : 'focus-within:border-accent focus-within:shadow-[var(--shadow-focus)]'
50
+ return (
51
+ <AriaTimeField
52
+ {...props}
53
+ className={(renderProps) =>
54
+ cn(
55
+ 'grid grid-cols-1 gap-1.5',
56
+ typeof className === 'function' ? className(renderProps) : className,
57
+ )
58
+ }
59
+ >
60
+ {label ? <Label className={fieldLabelStyles}>{label}</Label> : null}
61
+
62
+ <DateInput
63
+ className={cn(
64
+ styles.box(),
65
+ styles.content(),
66
+ 'flex items-center min-w-0 text-ink',
67
+ // Tabular alignment for time segments — overrides content's
68
+ // font-sans on purpose.
69
+ 'font-mono',
70
+ focusWithin,
71
+ )}
72
+ >
73
+ {(segment) => (
74
+ <DateSegment
75
+ segment={segment}
76
+ className={cn(
77
+ 'px-px tabular-nums outline-none select-none',
78
+ 'data-[placeholder]:text-ink-4',
79
+ 'data-[type=literal]:text-ink-4',
80
+ // segment focus treatment: indigo fill, paper text
81
+ 'data-[focused]:bg-accent data-[focused]:text-[#f0efe8] data-[focused]:rounded-[3px]',
82
+ 'data-[disabled]:cursor-not-allowed',
83
+ )}
84
+ />
85
+ )}
86
+ </DateInput>
87
+
88
+ {description ? (
89
+ <Text slot="description" className={fieldHintStyles}>
90
+ {description}
91
+ </Text>
92
+ ) : null}
93
+ {errorMessage && appearance !== 'cell' ? (
94
+ <FieldError className={fieldErrorStyles}>{errorMessage}</FieldError>
95
+ ) : null}
96
+ </AriaTimeField>
97
+ )
98
+ }
@@ -0,0 +1,2 @@
1
+ export type { TooltipProps } from './tooltip'
2
+ export { Tooltip, TooltipTrigger } from './tooltip'
@@ -0,0 +1,176 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+
3
+ import { Button } from '@/components/button/button'
4
+
5
+ import { Tooltip, TooltipTrigger } from './tooltip'
6
+
7
+ const meta: Meta<typeof TooltipTrigger> = {
8
+ title: 'Components/Tooltip',
9
+ component: TooltipTrigger,
10
+ tags: ['autodocs'],
11
+ parameters: { layout: 'padded' },
12
+ argTypes: {
13
+ delay: {
14
+ control: { type: 'number', min: 0, step: 50 },
15
+ description: 'Hover delay before the tooltip opens (ms). Focus ignores this.',
16
+ },
17
+ closeDelay: {
18
+ control: { type: 'number', min: 0, step: 50 },
19
+ description: 'Delay after pointer leaves before the tooltip closes (ms).',
20
+ },
21
+ },
22
+ }
23
+
24
+ export default meta
25
+ type Story = StoryObj<typeof TooltipTrigger>
26
+
27
+ const Info = (
28
+ <svg viewBox="0 0 16 16" fill="none" aria-hidden>
29
+ <circle cx="8" cy="8" r="6.5" stroke="currentColor" strokeWidth="1.2" />
30
+ <path d="M8 7.5v4M8 5v0.25" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
31
+ </svg>
32
+ )
33
+
34
+ export const Basic: Story = {
35
+ render: () => (
36
+ <TooltipTrigger>
37
+ <Button variant="ghost">Read manifesto</Button>
38
+ <Tooltip>Opens the one-pager on Proteos's design philosophy.</Tooltip>
39
+ </TooltipTrigger>
40
+ ),
41
+ }
42
+
43
+ export const OnIconButton: Story = {
44
+ render: () => (
45
+ <TooltipTrigger>
46
+ <Button size="icon-md" variant="ghost" aria-label="About">
47
+ {Info}
48
+ </Button>
49
+ <Tooltip>About Proteos</Tooltip>
50
+ </TooltipTrigger>
51
+ ),
52
+ }
53
+
54
+ export const WithShortcut: Story = {
55
+ render: () => (
56
+ <TooltipTrigger>
57
+ <Button variant="secondary">Compose</Button>
58
+ <Tooltip>
59
+ Compose system
60
+ <span className="ml-2 rounded-[3px] border border-rule bg-surface-2 px-1 py-px text-[10px] text-ink-3">
61
+ ⌘ ↵
62
+ </span>
63
+ </Tooltip>
64
+ </TooltipTrigger>
65
+ ),
66
+ }
67
+
68
+ /* =========================================================================
69
+ Delay presets — hover times to compare against the default (500ms).
70
+ ========================================================================= */
71
+
72
+ /** Opens the moment the pointer lands — good for icon buttons where the user
73
+ * is already hunting for the label. */
74
+ export const InstantDelay: Story = {
75
+ args: { delay: 0 },
76
+ render: (args) => (
77
+ <TooltipTrigger {...args}>
78
+ <Button size="icon-md" variant="ghost" aria-label="About">
79
+ {Info}
80
+ </Button>
81
+ <Tooltip>Opens with no delay — try hovering.</Tooltip>
82
+ </TooltipTrigger>
83
+ ),
84
+ }
85
+
86
+ /** Snappy but not twitchy — the package default. Documented here so you can
87
+ * eyeball it against the instant/slow extremes. */
88
+ export const SnappyDelay: Story = {
89
+ args: { delay: 500 },
90
+ render: (args) => (
91
+ <TooltipTrigger {...args}>
92
+ <Button variant="ghost">Hover me (500 ms)</Button>
93
+ <Tooltip>Default hover delay — balanced for dense UIs.</Tooltip>
94
+ </TooltipTrigger>
95
+ ),
96
+ }
97
+
98
+ /** Deliberately slow — mirrors RAC's out-of-the-box 1500ms. Useful when you
99
+ * want the tooltip to only show for users who *linger* on the target. */
100
+ export const SlowDelay: Story = {
101
+ args: { delay: 1500 },
102
+ render: (args) => (
103
+ <TooltipTrigger {...args}>
104
+ <Button variant="ghost">Hover me (1500 ms)</Button>
105
+ <Tooltip>Slow delay — only shows if you really pause on the target.</Tooltip>
106
+ </TooltipTrigger>
107
+ ),
108
+ }
109
+
110
+ /* =========================================================================
111
+ closeDelay in context — a row of adjacent targets. With `closeDelay` above
112
+ zero, moving between neighbours skips the re-open wait so the tooltips feel
113
+ like a chained sequence rather than four separate prompts.
114
+ ========================================================================= */
115
+
116
+ export const ChainedWithCloseDelay: Story = {
117
+ args: { delay: 300, closeDelay: 300 },
118
+ parameters: { controls: { hideNoControlsWarning: true } },
119
+ render: (args) => (
120
+ <div className="flex items-center gap-1">
121
+ <TooltipTrigger {...args}>
122
+ <Button size="icon-md" variant="ghost" aria-label="Filter">
123
+ {Info}
124
+ </Button>
125
+ <Tooltip>Filter</Tooltip>
126
+ </TooltipTrigger>
127
+ <TooltipTrigger {...args}>
128
+ <Button size="icon-md" variant="ghost" aria-label="Group">
129
+ {Info}
130
+ </Button>
131
+ <Tooltip>Group</Tooltip>
132
+ </TooltipTrigger>
133
+ <TooltipTrigger {...args}>
134
+ <Button size="icon-md" variant="ghost" aria-label="Sort">
135
+ {Info}
136
+ </Button>
137
+ <Tooltip>Sort</Tooltip>
138
+ </TooltipTrigger>
139
+ <TooltipTrigger {...args}>
140
+ <Button size="icon-md" variant="ghost" aria-label="Export">
141
+ {Info}
142
+ </Button>
143
+ <Tooltip>Export</Tooltip>
144
+ </TooltipTrigger>
145
+ </div>
146
+ ),
147
+ }
148
+
149
+ /** Opposite of the chain — each icon forces a fresh delay. Good for sparse
150
+ * toolbars where the tooltips aren't meant to read together. */
151
+ export const IsolatedNoCloseDelay: Story = {
152
+ args: { delay: 300, closeDelay: 0 },
153
+ parameters: { controls: { hideNoControlsWarning: true } },
154
+ render: (args) => (
155
+ <div className="flex items-center gap-1">
156
+ <TooltipTrigger {...args}>
157
+ <Button size="icon-md" variant="ghost" aria-label="Filter">
158
+ {Info}
159
+ </Button>
160
+ <Tooltip>Filter</Tooltip>
161
+ </TooltipTrigger>
162
+ <TooltipTrigger {...args}>
163
+ <Button size="icon-md" variant="ghost" aria-label="Group">
164
+ {Info}
165
+ </Button>
166
+ <Tooltip>Group</Tooltip>
167
+ </TooltipTrigger>
168
+ <TooltipTrigger {...args}>
169
+ <Button size="icon-md" variant="ghost" aria-label="Sort">
170
+ {Info}
171
+ </Button>
172
+ <Tooltip>Sort</Tooltip>
173
+ </TooltipTrigger>
174
+ </div>
175
+ ),
176
+ }
@@ -0,0 +1,52 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { describe, expect, it } from 'vitest'
4
+
5
+ import { Button } from '@/components/button/button'
6
+
7
+ import { Tooltip, TooltipTrigger } from './tooltip'
8
+
9
+ describe('Tooltip', () => {
10
+ it('is not visible by default', () => {
11
+ render(
12
+ <TooltipTrigger>
13
+ <Button>Info</Button>
14
+ <Tooltip>Helpful hint</Tooltip>
15
+ </TooltipTrigger>,
16
+ )
17
+ expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
18
+ })
19
+
20
+ it('appears when the trigger receives keyboard focus', async () => {
21
+ const user = userEvent.setup()
22
+ render(
23
+ <TooltipTrigger>
24
+ <Button>Info</Button>
25
+ <Tooltip>Helpful hint</Tooltip>
26
+ </TooltipTrigger>,
27
+ )
28
+
29
+ await user.tab()
30
+ expect(await screen.findByRole('tooltip')).toHaveTextContent('Helpful hint')
31
+ })
32
+
33
+ it('hides when focus leaves the trigger', async () => {
34
+ const user = userEvent.setup()
35
+ render(
36
+ <>
37
+ <TooltipTrigger>
38
+ <Button>Info</Button>
39
+ <Tooltip>Helpful hint</Tooltip>
40
+ </TooltipTrigger>
41
+ <Button>Other</Button>
42
+ </>,
43
+ )
44
+
45
+ await user.tab()
46
+ expect(await screen.findByRole('tooltip')).toBeInTheDocument()
47
+
48
+ await user.tab()
49
+ // Tooltip tears down once the trigger loses focus-visible.
50
+ expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
51
+ })
52
+ })
@@ -0,0 +1,56 @@
1
+ import {
2
+ Tooltip as AriaTooltip,
3
+ type TooltipProps as AriaTooltipProps,
4
+ TooltipTrigger as AriaTooltipTrigger,
5
+ type TooltipTriggerComponentProps,
6
+ } from 'react-aria-components'
7
+
8
+ import { cn } from '../../lib/cn'
9
+
10
+ /* =========================================================================
11
+ Tooltip — small mono label on hover/focus. Composition follows RAC:
12
+ <TooltipTrigger>
13
+ <Button>…</Button> // the hover/focus target
14
+ <Tooltip>Copy</Tooltip> // the floating label
15
+ </TooltipTrigger>
16
+ Use for icon-button labels, truncated text hints, keyboard shortcuts.
17
+ ========================================================================= */
18
+
19
+ /** Stateful root. RAC handles hover delay + focus visibility.
20
+ *
21
+ * - `delay` — ms of hover before the tooltip opens. Keyboard focus opens
22
+ * instantly regardless. Default **500** (RAC's own default is 1500 which
23
+ * feels sluggish for a dense app). Set `0` for instant tooltips.
24
+ * - `closeDelay` — ms after the pointer leaves before the tooltip hides.
25
+ * Default **0** (close immediately). Raise it if adjacent targets share
26
+ * tooltips and you want them to chain without re-waiting.
27
+ */
28
+ export function TooltipTrigger({
29
+ delay = 500,
30
+ closeDelay = 0,
31
+ ...props
32
+ }: TooltipTriggerComponentProps) {
33
+ return <AriaTooltipTrigger delay={delay} closeDelay={closeDelay} {...props} />
34
+ }
35
+
36
+ export type TooltipProps = AriaTooltipProps
37
+
38
+ export function Tooltip({ className, offset = 6, ...props }: TooltipProps) {
39
+ return (
40
+ <AriaTooltip
41
+ {...props}
42
+ offset={offset}
43
+ className={(renderProps) =>
44
+ cn(
45
+ 'z-50 max-w-xs',
46
+ 'rounded-md border border-rule bg-surface shadow-[var(--shadow-raised)]',
47
+ 'px-2.5 py-1.5',
48
+ 'font-mono text-[11.5px] text-ink-2',
49
+ 'entering:animate-in entering:fade-in-0 entering:slide-in-from-top-1 entering:duration-150',
50
+ 'exiting:animate-out exiting:fade-out-0 exiting:duration-100',
51
+ typeof className === 'function' ? className(renderProps) : className,
52
+ )
53
+ }
54
+ />
55
+ )
56
+ }
package/src/index.ts ADDED
@@ -0,0 +1,47 @@
1
+ export * from './components/attachment-chip'
2
+ export * from './components/attribute-type-icon'
3
+ export * from './components/avatar'
4
+ export * from './components/avatar-group'
5
+ export * from './components/badge'
6
+ export * from './components/badge-multi-select'
7
+ export * from './components/breadcrumbs'
8
+ export * from './components/button'
9
+ export * from './components/card'
10
+ export * from './components/checkbox'
11
+ export * from './components/combobox'
12
+ export * from './components/command'
13
+ export * from './components/currency-input'
14
+ export * from './components/data-table'
15
+ export * from './components/date-picker'
16
+ export * from './components/date-time-picker'
17
+ export * from './components/empty-state'
18
+ export * from './components/error-animation'
19
+ export * from './components/field'
20
+ export * from './components/file-field'
21
+ export * from './components/file-viewer'
22
+ export * from './components/image-attachment'
23
+ export * from './components/image-lightbox'
24
+ export * from './components/input'
25
+ export * from './components/kbd'
26
+ export * from './components/menu'
27
+ export * from './components/modal'
28
+ export * from './components/multi-select'
29
+ export * from './components/nav'
30
+ export * from './components/popover'
31
+ export * from './components/progress-bar'
32
+ export * from './components/radio-group'
33
+ export * from './components/record-filter'
34
+ export * from './components/segmented-control'
35
+ export * from './components/select'
36
+ export * from './components/separator'
37
+ export * from './components/skeleton'
38
+ export * from './components/success-animation'
39
+ export * from './components/switch'
40
+ export * from './components/table'
41
+ export * from './components/tabs'
42
+ export * from './components/tag-input'
43
+ export * from './components/textarea'
44
+ export * from './components/time-field'
45
+ export * from './components/tooltip'
46
+
47
+ export { cn } from './lib/cn'
package/src/lib/cn.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]): string {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,76 @@
1
+ import { act, render, screen } from '@testing-library/react'
2
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
3
+
4
+ import {
5
+ _getDiscoverySubscriberCount,
6
+ _resetDiscovery,
7
+ useDiscoveryActive,
8
+ } from './discovery-store'
9
+ import { _setPlatformForTests } from './platform'
10
+
11
+ function Probe() {
12
+ const active = useDiscoveryActive()
13
+ return <span data-testid="probe">{active ? 'on' : 'off'}</span>
14
+ }
15
+
16
+ function holdCmdShift() {
17
+ act(() => {
18
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Shift', shiftKey: true }))
19
+ window.dispatchEvent(
20
+ new KeyboardEvent('keydown', { key: 'Meta', shiftKey: true, metaKey: true }),
21
+ )
22
+ })
23
+ }
24
+
25
+ function releaseMeta() {
26
+ act(() => {
27
+ window.dispatchEvent(new KeyboardEvent('keyup', { key: 'Meta', shiftKey: true }))
28
+ })
29
+ }
30
+
31
+ describe('useDiscoveryActive', () => {
32
+ beforeEach(() => {
33
+ _setPlatformForTests('mac')
34
+ _resetDiscovery()
35
+ })
36
+ afterEach(() => {
37
+ _setPlatformForTests(undefined)
38
+ _resetDiscovery()
39
+ })
40
+
41
+ it('is false initially', () => {
42
+ render(<Probe />)
43
+ expect(screen.getByTestId('probe').textContent).toBe('off')
44
+ })
45
+
46
+ it('flips on when Cmd+Shift are both held', () => {
47
+ render(<Probe />)
48
+ holdCmdShift()
49
+ expect(screen.getByTestId('probe').textContent).toBe('on')
50
+ })
51
+
52
+ it('flips off when one modifier is released', () => {
53
+ render(<Probe />)
54
+ holdCmdShift()
55
+ releaseMeta()
56
+ expect(screen.getByTestId('probe').textContent).toBe('off')
57
+ })
58
+
59
+ it('keeps a single subscriber per mounted Probe (one listener total at the module level)', () => {
60
+ const { unmount: u1 } = render(<Probe />)
61
+ const { unmount: u2 } = render(<Probe />)
62
+ expect(_getDiscoverySubscriberCount()).toBe(2)
63
+ u1()
64
+ u2()
65
+ expect(_getDiscoverySubscriberCount()).toBe(0)
66
+ })
67
+
68
+ it('resets on blur (alt-tab scenario)', () => {
69
+ render(<Probe />)
70
+ holdCmdShift()
71
+ act(() => {
72
+ window.dispatchEvent(new Event('blur'))
73
+ })
74
+ expect(screen.getByTestId('probe').textContent).toBe('off')
75
+ })
76
+ })
@@ -0,0 +1,110 @@
1
+ /* Module-singleton store tracking the Cmd+Shift "discovery hold". One pair
2
+ of window listeners regardless of how many <Command> wrappers subscribe.
3
+ Subscribers re-render via useSyncExternalStore when the both-held flag
4
+ flips. Resyncs from event modifier flags on every key event so it stays
5
+ accurate even if the browser drops a modifier key event (focus changes,
6
+ alt-tab, etc.). */
7
+
8
+ import { useSyncExternalStore } from 'react'
9
+
10
+ import { getPlatform } from './platform'
11
+
12
+ type Listener = () => void
13
+
14
+ let cmdHeld = false
15
+ let shiftHeld = false
16
+ let initialized = false
17
+ const listeners = new Set<Listener>()
18
+
19
+ function getSnapshot(): boolean {
20
+ return cmdHeld && shiftHeld
21
+ }
22
+
23
+ function getServerSnapshot(): boolean {
24
+ return false
25
+ }
26
+
27
+ function emit(): void {
28
+ for (const l of listeners) l()
29
+ }
30
+
31
+ function sync(cmdNow: boolean, shiftNow: boolean): void {
32
+ if (cmdNow !== cmdHeld || shiftNow !== shiftHeld) {
33
+ cmdHeld = cmdNow
34
+ shiftHeld = shiftNow
35
+ emit()
36
+ }
37
+ }
38
+
39
+ function onKeyDown(e: KeyboardEvent): void {
40
+ const platform = getPlatform()
41
+ const cmdFlag = platform === 'mac' ? e.metaKey : e.ctrlKey
42
+ // For the modifier key itself, trust the event type (the flag lags on press).
43
+ if (e.key === 'Meta' || e.key === 'Control') {
44
+ sync(true, e.shiftKey)
45
+ return
46
+ }
47
+ if (e.key === 'Shift') {
48
+ sync(cmdFlag, true)
49
+ return
50
+ }
51
+ sync(cmdFlag, e.shiftKey)
52
+ }
53
+
54
+ function onKeyUp(e: KeyboardEvent): void {
55
+ const platform = getPlatform()
56
+ const cmdFlag = platform === 'mac' ? e.metaKey : e.ctrlKey
57
+ if (e.key === 'Meta' || e.key === 'Control') {
58
+ sync(false, e.shiftKey)
59
+ return
60
+ }
61
+ if (e.key === 'Shift') {
62
+ sync(cmdFlag, false)
63
+ return
64
+ }
65
+ sync(cmdFlag, e.shiftKey)
66
+ }
67
+
68
+ function onBlur(): void {
69
+ // The window lost focus — assume the user released everything. Without
70
+ // this, holding Cmd+Shift then alt-tabbing would leave us stuck in
71
+ // discovery mode until the next keydown.
72
+ sync(false, false)
73
+ }
74
+
75
+ function ensureInitialized(): void {
76
+ if (initialized) return
77
+ if (typeof window === 'undefined') return
78
+ window.addEventListener('keydown', onKeyDown, { capture: true })
79
+ window.addEventListener('keyup', onKeyUp, { capture: true })
80
+ window.addEventListener('blur', onBlur)
81
+ initialized = true
82
+ }
83
+
84
+ function subscribe(listener: Listener): () => void {
85
+ ensureInitialized()
86
+ listeners.add(listener)
87
+ return () => {
88
+ listeners.delete(listener)
89
+ }
90
+ }
91
+
92
+ export function useDiscoveryActive(): boolean {
93
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
94
+ }
95
+
96
+ // Test-only: number of installed listeners is the number of subscribers.
97
+ // The window-level listeners are installed at most once (initialized flag).
98
+ export function _getDiscoverySubscriberCount(): number {
99
+ return listeners.size
100
+ }
101
+
102
+ export function _resetDiscovery(): void {
103
+ cmdHeld = false
104
+ shiftHeld = false
105
+ listeners.clear()
106
+ }
107
+
108
+ export function _isDiscoveryInitialized(): boolean {
109
+ return initialized
110
+ }
@@ -0,0 +1,9 @@
1
+ export type { KeyCombo } from './parse-combo'
2
+ export { parseCombo } from './parse-combo'
3
+ export { PROTECTED_COMBOS } from './protected-combos'
4
+ export { isEditableTarget } from './match-event'
5
+ export type { UseCommandOptions } from './use-command'
6
+ export { useCommand } from './use-command'
7
+ export type { UseEscapeKeyOptions } from './use-escape-key'
8
+ export { useEscapeKey } from './use-escape-key'
9
+ export { useDiscoveryActive } from './discovery-store'