@lovett/ui 0.0.10 → 0.1.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 (158) hide show
  1. package/dist/chunk-RBYWGBQ2.js +2752 -0
  2. package/dist/chunk-RBYWGBQ2.js.map +1 -0
  3. package/dist/index.d.ts +6064 -80
  4. package/dist/index.js +21561 -9696
  5. package/dist/index.js.map +1 -1
  6. package/dist/rich-composer-impl-5NO443A6.js +1859 -0
  7. package/dist/rich-composer-impl-5NO443A6.js.map +1 -0
  8. package/dist/styles.css +1586 -2
  9. package/dist/theme-v2.css +228 -0
  10. package/dist/tokens.css +121 -0
  11. package/package.json +8 -1
  12. package/src/__tests__/anchor.test.tsx +422 -0
  13. package/src/__tests__/avatar.test.tsx +272 -0
  14. package/src/__tests__/bar-chart.test.tsx +809 -0
  15. package/src/__tests__/board.test.tsx +420 -0
  16. package/src/__tests__/chart-math.test.ts +922 -0
  17. package/src/__tests__/chart-series.test.ts +339 -0
  18. package/src/__tests__/code-block.test.tsx +134 -0
  19. package/src/__tests__/combobox.test.tsx +677 -0
  20. package/src/__tests__/display-popover.test.tsx +195 -0
  21. package/src/__tests__/display-store.test.tsx +208 -0
  22. package/src/__tests__/donut-chart.test.tsx +397 -0
  23. package/src/__tests__/dropdown-menu.test.tsx +572 -0
  24. package/src/__tests__/filter-menu.test.tsx +175 -0
  25. package/src/__tests__/gauge-ring.test.tsx +233 -0
  26. package/src/__tests__/helpers/geometry.ts +58 -0
  27. package/src/__tests__/layer-stack.test.tsx +228 -0
  28. package/src/__tests__/line-chart.test.tsx +612 -0
  29. package/src/__tests__/popover.test.tsx +460 -0
  30. package/src/__tests__/ranked-bars.test.tsx +343 -0
  31. package/src/__tests__/remark-underline.test.ts +194 -0
  32. package/src/__tests__/select.test.tsx +543 -0
  33. package/src/__tests__/sparkline.test.tsx +368 -0
  34. package/src/__tests__/stat-layer.test.tsx +271 -0
  35. package/src/__tests__/stat-strip.test.tsx +175 -0
  36. package/src/__tests__/status.test.tsx +293 -0
  37. package/src/__tests__/tabs.test.tsx +303 -0
  38. package/src/__tests__/token-shape.test.ts +132 -2
  39. package/src/__tests__/tooltip.test.tsx +355 -0
  40. package/src/avatar.tsx +352 -0
  41. package/src/bar-chart.tsx +1214 -0
  42. package/src/board.tsx +658 -0
  43. package/src/calculator-shell-v2.tsx +19 -39
  44. package/src/chart-frame.tsx +960 -0
  45. package/src/chart-legend.tsx +304 -0
  46. package/src/chart-tooltip.tsx +267 -0
  47. package/src/code-block.tsx +62 -8
  48. package/src/combobox.tsx +796 -0
  49. package/src/delta-chip.tsx +263 -0
  50. package/src/detail/__tests__/activity-pane.test.tsx +186 -0
  51. package/src/detail/__tests__/detail-chrome.test.tsx +134 -0
  52. package/src/detail/__tests__/detail-surface.test.tsx +529 -0
  53. package/src/detail/__tests__/field-row.test.tsx +357 -0
  54. package/src/detail/activity-pane.tsx +507 -0
  55. package/src/detail/calendar.tsx +355 -0
  56. package/src/detail/detail-divider.tsx +261 -0
  57. package/src/detail/detail-header.tsx +287 -0
  58. package/src/detail/detail-menu.tsx +254 -0
  59. package/src/detail/detail-surface.tsx +1110 -0
  60. package/src/detail/field-list.tsx +196 -0
  61. package/src/detail/field-row.tsx +1131 -0
  62. package/src/detail/index.ts +58 -0
  63. package/src/detail/segmented-choice.tsx +94 -0
  64. package/src/detail/types.ts +129 -0
  65. package/src/display-popover.tsx +487 -0
  66. package/src/display-store.tsx +242 -0
  67. package/src/donut-chart.tsx +988 -0
  68. package/src/dropdown-menu.tsx +412 -151
  69. package/src/filter-core/EXPORTS.md +85 -0
  70. package/src/filter-core/__tests__/columns.test.ts +159 -0
  71. package/src/filter-core/__tests__/faceting.test.ts +193 -0
  72. package/src/filter-core/__tests__/filter-fns.test.ts +519 -0
  73. package/src/filter-core/__tests__/operators.test.ts +235 -0
  74. package/src/filter-core/__tests__/state.test.ts +268 -0
  75. package/src/filter-core/__tests__/url.test.ts +350 -0
  76. package/src/filter-core/columns.ts +134 -0
  77. package/src/filter-core/date-utils.ts +38 -0
  78. package/src/filter-core/examples/task-filter-columns.ts +121 -0
  79. package/src/filter-core/faceting.ts +120 -0
  80. package/src/filter-core/filter-fns.ts +335 -0
  81. package/src/filter-core/index.ts +105 -0
  82. package/src/filter-core/operators.ts +433 -0
  83. package/src/filter-core/state.ts +280 -0
  84. package/src/filter-core/types.ts +247 -0
  85. package/src/filter-core/url.ts +261 -0
  86. package/src/filter-dropdown.tsx +12 -0
  87. package/src/filter-menu.tsx +649 -0
  88. package/src/floating-drawer.tsx +19 -1
  89. package/src/gauge-ring.tsx +435 -0
  90. package/src/hue.ts +52 -0
  91. package/src/index.ts +402 -0
  92. package/src/kbd.tsx +27 -4
  93. package/src/lib/anchor.ts +427 -0
  94. package/src/lib/chart.ts +866 -0
  95. package/src/lib/focus.ts +74 -0
  96. package/src/lib/layer-stack.ts +215 -0
  97. package/src/lib/refs.ts +31 -0
  98. package/src/lib/remark-underline.ts +443 -0
  99. package/src/lib/series.ts +169 -0
  100. package/src/line-chart.tsx +1176 -0
  101. package/src/markdown.tsx +26 -7
  102. package/src/metric-card.tsx +57 -22
  103. package/src/modal.tsx +73 -66
  104. package/src/popover.tsx +407 -0
  105. package/src/progress-ledger.tsx +304 -0
  106. package/src/ranked-bars.tsx +386 -0
  107. package/src/segmented-pill.tsx +32 -9
  108. package/src/select.tsx +646 -0
  109. package/src/sortable.tsx +315 -1
  110. package/src/sparkline.tsx +416 -0
  111. package/src/stat-card.tsx +376 -0
  112. package/src/stat-row.tsx +108 -70
  113. package/src/stat-strip.tsx +327 -0
  114. package/src/status.tsx +215 -0
  115. package/src/styles.css +1586 -2
  116. package/src/tabs.tsx +206 -25
  117. package/src/task-card.tsx +610 -0
  118. package/src/thread/__tests__/comment-body-hostile.test.tsx +331 -0
  119. package/src/thread/__tests__/comment-tree.test.ts +151 -0
  120. package/src/thread/__tests__/emoji.test.ts +187 -0
  121. package/src/thread/__tests__/fixtures/thread-fixture.ts +235 -0
  122. package/src/thread/__tests__/link-preview-source.test.ts +120 -0
  123. package/src/thread/__tests__/link-preview.test.tsx +600 -0
  124. package/src/thread/__tests__/markdown-format.test.ts +82 -0
  125. package/src/thread/__tests__/markdown-spec.test.ts +469 -0
  126. package/src/thread/__tests__/relative-time.test.ts +71 -0
  127. package/src/thread/__tests__/rich-composer.test.tsx +799 -0
  128. package/src/thread/__tests__/scroll-caret.test.ts +58 -0
  129. package/src/thread/__tests__/suggestion-list.test.tsx +648 -0
  130. package/src/thread/__tests__/thread-scroll-ownership.test.tsx +88 -0
  131. package/src/thread/__tests__/thread.test.tsx +662 -0
  132. package/src/thread/__tests__/use-attachments.test.tsx +679 -0
  133. package/src/thread/actions.tsx +196 -0
  134. package/src/thread/attachments.tsx +1071 -0
  135. package/src/thread/comment-body.tsx +148 -0
  136. package/src/thread/comment-tree.ts +182 -0
  137. package/src/thread/comment.tsx +915 -0
  138. package/src/thread/composer-footer.tsx +125 -0
  139. package/src/thread/composer.tsx +319 -0
  140. package/src/thread/emoji.ts +283 -0
  141. package/src/thread/index.ts +153 -0
  142. package/src/thread/link-preview.tsx +341 -0
  143. package/src/thread/markdown-format.ts +155 -0
  144. package/src/thread/markdown-spec.ts +754 -0
  145. package/src/thread/rail.tsx +372 -0
  146. package/src/thread/reactions.tsx +415 -0
  147. package/src/thread/relative-time.tsx +94 -0
  148. package/src/thread/rich-composer-impl.tsx +1601 -0
  149. package/src/thread/rich-composer.tsx +195 -0
  150. package/src/thread/scroll-caret.ts +37 -0
  151. package/src/thread/suggestion-list.tsx +182 -0
  152. package/src/thread/thread.tsx +718 -0
  153. package/src/thread/types.ts +218 -0
  154. package/src/thread/use-attachments.ts +598 -0
  155. package/src/thread/use-now.ts +73 -0
  156. package/src/thread/use-thread.ts +316 -0
  157. package/src/tokens.css +121 -0
  158. package/src/tooltip.tsx +297 -0
@@ -0,0 +1,228 @@
1
+ /**
2
+ * lib/layer-stack — the topmost-wins dismiss order shared by Modal,
3
+ * DropdownMenu and Popover.
4
+ *
5
+ * The load-bearing test is the Modal one: a layer registered AFTER a Modal
6
+ * opens takes Escape away from it. Revert Modal to a private `openPanels`
7
+ * array and both fire — that test goes red.
8
+ */
9
+ import { useEffect } from 'react'
10
+ import { describe, expect, it, vi } from 'vitest'
11
+ import { render } from '@testing-library/react'
12
+ import userEvent from '@testing-library/user-event'
13
+ import Modal from '../modal'
14
+ import { useEscapeKey, useLayer, type LayerHandle, type LayerKind } from '../lib/layer-stack'
15
+
16
+ function EscapeListener({
17
+ onEscape,
18
+ enabled = true,
19
+ }: {
20
+ onEscape: () => void
21
+ enabled?: boolean
22
+ }) {
23
+ useEscapeKey(onEscape, { enabled })
24
+ return null
25
+ }
26
+
27
+ function LayerProbe({
28
+ kind,
29
+ onHandle,
30
+ }: {
31
+ kind: LayerKind
32
+ onHandle: (handle: LayerHandle) => void
33
+ }) {
34
+ const handle = useLayer({ enabled: true, kind, onEscape: () => {} })
35
+ useEffect(() => {
36
+ onHandle(handle)
37
+ }, [handle, onHandle])
38
+ return null
39
+ }
40
+
41
+ describe('useEscapeKey / useLayer', () => {
42
+ it('dispatches Escape to the most recently registered layer only', async () => {
43
+ const first = vi.fn()
44
+ const second = vi.fn()
45
+ render(
46
+ <>
47
+ <EscapeListener onEscape={first} />
48
+ <EscapeListener onEscape={second} />
49
+ </>,
50
+ )
51
+ await userEvent.keyboard('{Escape}')
52
+ expect(second).toHaveBeenCalledTimes(1)
53
+ expect(first).not.toHaveBeenCalled()
54
+ })
55
+
56
+ it('falls back to the layer below when the top one unregisters', async () => {
57
+ const first = vi.fn()
58
+ const second = vi.fn()
59
+ const { rerender } = render(
60
+ <>
61
+ <EscapeListener onEscape={first} />
62
+ <EscapeListener onEscape={second} />
63
+ </>,
64
+ )
65
+ rerender(
66
+ <>
67
+ <EscapeListener onEscape={first} />
68
+ <EscapeListener onEscape={second} enabled={false} />
69
+ </>,
70
+ )
71
+ await userEvent.keyboard('{Escape}')
72
+ expect(first).toHaveBeenCalledTimes(1)
73
+ expect(second).not.toHaveBeenCalled()
74
+ })
75
+
76
+ it('does nothing once every layer has unregistered', async () => {
77
+ const handler = vi.fn()
78
+ const { unmount } = render(<EscapeListener onEscape={handler} />)
79
+ unmount()
80
+ await userEvent.keyboard('{Escape}')
81
+ expect(handler).not.toHaveBeenCalled()
82
+ })
83
+
84
+ it('ignores keys other than Escape', async () => {
85
+ const handler = vi.fn()
86
+ render(<EscapeListener onEscape={handler} />)
87
+ await userEvent.keyboard('{Enter}a{Tab}')
88
+ expect(handler).not.toHaveBeenCalled()
89
+ })
90
+
91
+ it('stops propagation so window-level listeners do not see a consumed Escape', async () => {
92
+ const windowSpy = vi.fn()
93
+ window.addEventListener('keydown', windowSpy)
94
+ try {
95
+ const handler = vi.fn()
96
+ const { unmount } = render(<EscapeListener onEscape={handler} />)
97
+ await userEvent.keyboard('{Escape}')
98
+ expect(handler).toHaveBeenCalledTimes(1)
99
+ expect(windowSpy).not.toHaveBeenCalled()
100
+
101
+ // With no layer open the key reaches window as before.
102
+ unmount()
103
+ await userEvent.keyboard('{Escape}')
104
+ expect(windowSpy).toHaveBeenCalledTimes(1)
105
+ } finally {
106
+ window.removeEventListener('keydown', windowSpy)
107
+ }
108
+ })
109
+
110
+ it('reads the latest handler without re-registering (order is preserved)', async () => {
111
+ const stale = vi.fn()
112
+ const fresh = vi.fn()
113
+ const above = vi.fn()
114
+ const { rerender } = render(
115
+ <>
116
+ <EscapeListener onEscape={stale} />
117
+ <EscapeListener onEscape={above} />
118
+ </>,
119
+ )
120
+ // Swapping the LOWER layer's handler must not move it to the top.
121
+ rerender(
122
+ <>
123
+ <EscapeListener onEscape={fresh} />
124
+ <EscapeListener onEscape={above} />
125
+ </>,
126
+ )
127
+ await userEvent.keyboard('{Escape}')
128
+ expect(above).toHaveBeenCalledTimes(1)
129
+ expect(stale).not.toHaveBeenCalled()
130
+ expect(fresh).not.toHaveBeenCalled()
131
+
132
+ rerender(
133
+ <>
134
+ <EscapeListener onEscape={fresh} />
135
+ <EscapeListener onEscape={above} enabled={false} />
136
+ </>,
137
+ )
138
+ await userEvent.keyboard('{Escape}')
139
+ expect(fresh).toHaveBeenCalledTimes(1)
140
+ expect(stale).not.toHaveBeenCalled()
141
+ })
142
+
143
+ it('isTop / isTopOfKind: a popover above a modal is top, but not top-of-kind for the modal', () => {
144
+ let modal: LayerHandle | undefined
145
+ let popover: LayerHandle | undefined
146
+ render(
147
+ <>
148
+ <LayerProbe kind="modal" onHandle={(h) => (modal = h)} />
149
+ <LayerProbe kind="popover" onHandle={(h) => (popover = h)} />
150
+ </>,
151
+ )
152
+ expect(popover?.isTop()).toBe(true)
153
+ expect(popover?.isTopOfKind()).toBe(true)
154
+ expect(modal?.isTop()).toBe(false)
155
+ expect(modal?.isTopOfKind()).toBe(true)
156
+ })
157
+
158
+ it('a second modal above the first takes both isTop and isTopOfKind', () => {
159
+ let lower: LayerHandle | undefined
160
+ let upper: LayerHandle | undefined
161
+ render(
162
+ <>
163
+ <LayerProbe kind="modal" onHandle={(h) => (lower = h)} />
164
+ <LayerProbe kind="modal" onHandle={(h) => (upper = h)} />
165
+ </>,
166
+ )
167
+ expect(upper?.isTop()).toBe(true)
168
+ expect(lower?.isTopOfKind()).toBe(false)
169
+ })
170
+
171
+ it('an unregistered handle answers false everywhere', () => {
172
+ let handle: LayerHandle | undefined
173
+ function Disabled() {
174
+ handle = useLayer({ enabled: false, kind: 'popover', onEscape: () => {} })
175
+ return null
176
+ }
177
+ render(<Disabled />)
178
+ expect(handle?.isTop()).toBe(false)
179
+ expect(handle?.isTopOfKind()).toBe(false)
180
+ expect(handle?.containsInLayerAbove(document.body)).toBe(false)
181
+ })
182
+ })
183
+
184
+ describe('shared with Modal', () => {
185
+ it('a layer opened above a Modal takes Escape; the Modal gets the next one', async () => {
186
+ const onClose = vi.fn()
187
+ const onEscape = vi.fn()
188
+ const { rerender } = render(
189
+ <>
190
+ <Modal isOpen onClose={onClose} title="Confirm">
191
+ body
192
+ </Modal>
193
+ <EscapeListener onEscape={onEscape} />
194
+ </>,
195
+ )
196
+ await userEvent.keyboard('{Escape}')
197
+ expect(onEscape).toHaveBeenCalledTimes(1)
198
+ expect(onClose).not.toHaveBeenCalled()
199
+
200
+ rerender(
201
+ <>
202
+ <Modal isOpen onClose={onClose} title="Confirm">
203
+ body
204
+ </Modal>
205
+ <EscapeListener onEscape={onEscape} enabled={false} />
206
+ </>,
207
+ )
208
+ await userEvent.keyboard('{Escape}')
209
+ expect(onClose).toHaveBeenCalledTimes(1)
210
+ expect(onEscape).toHaveBeenCalledTimes(1)
211
+ })
212
+
213
+ it('a Modal opened above an existing layer takes Escape from it', async () => {
214
+ const onClose = vi.fn()
215
+ const onEscape = vi.fn()
216
+ render(
217
+ <>
218
+ <EscapeListener onEscape={onEscape} />
219
+ <Modal isOpen onClose={onClose} title="Confirm">
220
+ body
221
+ </Modal>
222
+ </>,
223
+ )
224
+ await userEvent.keyboard('{Escape}')
225
+ expect(onClose).toHaveBeenCalledTimes(1)
226
+ expect(onEscape).not.toHaveBeenCalled()
227
+ })
228
+ })