@bitvea/feedback-toolbar 0.0.1

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 (176) hide show
  1. package/README.md +262 -0
  2. package/dist/FeedbackPanel.d.ts +117 -0
  3. package/dist/FeedbackPanel.d.ts.map +1 -0
  4. package/dist/FeedbackPanel.js +622 -0
  5. package/dist/FeedbackPanel.js.map +1 -0
  6. package/dist/FeedbackToolbar.d.ts +54 -0
  7. package/dist/FeedbackToolbar.d.ts.map +1 -0
  8. package/dist/FeedbackToolbar.js +1149 -0
  9. package/dist/FeedbackToolbar.js.map +1 -0
  10. package/dist/InboxSheet.d.ts +80 -0
  11. package/dist/InboxSheet.d.ts.map +1 -0
  12. package/dist/InboxSheet.js +580 -0
  13. package/dist/InboxSheet.js.map +1 -0
  14. package/dist/PinLayer.d.ts +59 -0
  15. package/dist/PinLayer.d.ts.map +1 -0
  16. package/dist/PinLayer.js +1702 -0
  17. package/dist/PinLayer.js.map +1 -0
  18. package/dist/RichText.d.ts +6 -0
  19. package/dist/RichText.d.ts.map +1 -0
  20. package/dist/RichText.js +135 -0
  21. package/dist/RichText.js.map +1 -0
  22. package/dist/browser/entry.d.ts +3 -0
  23. package/dist/browser/entry.d.ts.map +1 -0
  24. package/dist/browser/entry.js +13 -0
  25. package/dist/browser/entry.js.map +1 -0
  26. package/dist/browser/mount.d.ts +37 -0
  27. package/dist/browser/mount.d.ts.map +1 -0
  28. package/dist/browser/mount.js +150 -0
  29. package/dist/browser/mount.js.map +1 -0
  30. package/dist/browser/toolbar.js +38 -0
  31. package/dist/browser/toolbar.js.map +7 -0
  32. package/dist/capture.d.ts +58 -0
  33. package/dist/capture.d.ts.map +1 -0
  34. package/dist/capture.js +98 -0
  35. package/dist/capture.js.map +1 -0
  36. package/dist/cli/bin.d.ts +3 -0
  37. package/dist/cli/bin.d.ts.map +1 -0
  38. package/dist/cli/bin.js +196 -0
  39. package/dist/cli/bin.js.map +1 -0
  40. package/dist/cli/fs.d.ts +16 -0
  41. package/dist/cli/fs.d.ts.map +1 -0
  42. package/dist/cli/fs.js +33 -0
  43. package/dist/cli/fs.js.map +1 -0
  44. package/dist/cli/init.d.ts +75 -0
  45. package/dist/cli/init.d.ts.map +1 -0
  46. package/dist/cli/init.js +266 -0
  47. package/dist/cli/init.js.map +1 -0
  48. package/dist/cli/register.d.ts +55 -0
  49. package/dist/cli/register.d.ts.map +1 -0
  50. package/dist/cli/register.js +84 -0
  51. package/dist/cli/register.js.map +1 -0
  52. package/dist/cli/registration.d.ts +37 -0
  53. package/dist/cli/registration.d.ts.map +1 -0
  54. package/dist/cli/registration.js +69 -0
  55. package/dist/cli/registration.js.map +1 -0
  56. package/dist/cli/strategies.d.ts +98 -0
  57. package/dist/cli/strategies.d.ts.map +1 -0
  58. package/dist/cli/strategies.js +198 -0
  59. package/dist/cli/strategies.js.map +1 -0
  60. package/dist/client.d.ts +59 -0
  61. package/dist/client.d.ts.map +1 -0
  62. package/dist/client.js +113 -0
  63. package/dist/client.js.map +1 -0
  64. package/dist/config.d.ts +92 -0
  65. package/dist/config.d.ts.map +1 -0
  66. package/dist/config.js +65 -0
  67. package/dist/config.js.map +1 -0
  68. package/dist/deepLink.d.ts +86 -0
  69. package/dist/deepLink.d.ts.map +1 -0
  70. package/dist/deepLink.js +185 -0
  71. package/dist/deepLink.js.map +1 -0
  72. package/dist/fontFace.d.ts +70 -0
  73. package/dist/fontFace.d.ts.map +1 -0
  74. package/dist/fontFace.js +88 -0
  75. package/dist/fontFace.js.map +1 -0
  76. package/dist/gating.d.ts +32 -0
  77. package/dist/gating.d.ts.map +1 -0
  78. package/dist/gating.js +49 -0
  79. package/dist/gating.js.map +1 -0
  80. package/dist/icons.d.ts +68 -0
  81. package/dist/icons.d.ts.map +1 -0
  82. package/dist/icons.js +97 -0
  83. package/dist/icons.js.map +1 -0
  84. package/dist/index.d.ts +14 -0
  85. package/dist/index.d.ts.map +1 -0
  86. package/dist/index.js +68 -0
  87. package/dist/index.js.map +1 -0
  88. package/dist/markdown.d.ts +59 -0
  89. package/dist/markdown.d.ts.map +1 -0
  90. package/dist/markdown.js +130 -0
  91. package/dist/markdown.js.map +1 -0
  92. package/dist/motion.d.ts +178 -0
  93. package/dist/motion.d.ts.map +1 -0
  94. package/dist/motion.js +302 -0
  95. package/dist/motion.js.map +1 -0
  96. package/dist/preferences.d.ts +71 -0
  97. package/dist/preferences.d.ts.map +1 -0
  98. package/dist/preferences.js +195 -0
  99. package/dist/preferences.js.map +1 -0
  100. package/dist/rasterise.d.ts +45 -0
  101. package/dist/rasterise.d.ts.map +1 -0
  102. package/dist/rasterise.js +490 -0
  103. package/dist/rasterise.js.map +1 -0
  104. package/dist/selector.d.ts +19 -0
  105. package/dist/selector.d.ts.map +1 -0
  106. package/dist/selector.js +142 -0
  107. package/dist/selector.js.map +1 -0
  108. package/dist/session.d.ts +19 -0
  109. package/dist/session.d.ts.map +1 -0
  110. package/dist/session.js +36 -0
  111. package/dist/session.js.map +1 -0
  112. package/dist/shortcut.d.ts +88 -0
  113. package/dist/shortcut.d.ts.map +1 -0
  114. package/dist/shortcut.js +149 -0
  115. package/dist/shortcut.js.map +1 -0
  116. package/dist/tokens.d.ts +709 -0
  117. package/dist/tokens.d.ts.map +1 -0
  118. package/dist/tokens.js +749 -0
  119. package/dist/tokens.js.map +1 -0
  120. package/dist/vendor/types/account.d.ts +26 -0
  121. package/dist/vendor/types/account.d.ts.map +1 -0
  122. package/dist/vendor/types/account.js +23 -0
  123. package/dist/vendor/types/account.js.map +1 -0
  124. package/dist/vendor/types/apiKey.d.ts +38 -0
  125. package/dist/vendor/types/apiKey.d.ts.map +1 -0
  126. package/dist/vendor/types/apiKey.js +36 -0
  127. package/dist/vendor/types/apiKey.js.map +1 -0
  128. package/dist/vendor/types/auth.d.ts +10 -0
  129. package/dist/vendor/types/auth.d.ts.map +1 -0
  130. package/dist/vendor/types/auth.js +15 -0
  131. package/dist/vendor/types/auth.js.map +1 -0
  132. package/dist/vendor/types/entity.d.ts +74 -0
  133. package/dist/vendor/types/entity.d.ts.map +1 -0
  134. package/dist/vendor/types/entity.js +111 -0
  135. package/dist/vendor/types/entity.js.map +1 -0
  136. package/dist/vendor/types/feedback.d.ts +285 -0
  137. package/dist/vendor/types/feedback.d.ts.map +1 -0
  138. package/dist/vendor/types/feedback.js +286 -0
  139. package/dist/vendor/types/feedback.js.map +1 -0
  140. package/dist/vendor/types/index.d.ts +14 -0
  141. package/dist/vendor/types/index.d.ts.map +1 -0
  142. package/dist/vendor/types/index.js +16 -0
  143. package/dist/vendor/types/index.js.map +1 -0
  144. package/dist/vendor/types/invite.d.ts +42 -0
  145. package/dist/vendor/types/invite.d.ts.map +1 -0
  146. package/dist/vendor/types/invite.js +30 -0
  147. package/dist/vendor/types/invite.js.map +1 -0
  148. package/dist/vendor/types/mcpToken.d.ts +38 -0
  149. package/dist/vendor/types/mcpToken.d.ts.map +1 -0
  150. package/dist/vendor/types/mcpToken.js +39 -0
  151. package/dist/vendor/types/mcpToken.js.map +1 -0
  152. package/dist/vendor/types/organization.d.ts +13 -0
  153. package/dist/vendor/types/organization.d.ts.map +1 -0
  154. package/dist/vendor/types/organization.js +12 -0
  155. package/dist/vendor/types/organization.js.map +1 -0
  156. package/dist/vendor/types/project.d.ts +73 -0
  157. package/dist/vendor/types/project.d.ts.map +1 -0
  158. package/dist/vendor/types/project.js +117 -0
  159. package/dist/vendor/types/project.js.map +1 -0
  160. package/dist/vendor/types/role.d.ts +12 -0
  161. package/dist/vendor/types/role.d.ts.map +1 -0
  162. package/dist/vendor/types/role.js +20 -0
  163. package/dist/vendor/types/role.js.map +1 -0
  164. package/dist/vendor/types/roleDefinition.d.ts +61 -0
  165. package/dist/vendor/types/roleDefinition.d.ts.map +1 -0
  166. package/dist/vendor/types/roleDefinition.js +45 -0
  167. package/dist/vendor/types/roleDefinition.js.map +1 -0
  168. package/dist/vendor/types/team.d.ts +25 -0
  169. package/dist/vendor/types/team.d.ts.map +1 -0
  170. package/dist/vendor/types/team.js +17 -0
  171. package/dist/vendor/types/team.js.map +1 -0
  172. package/dist/vendor/types/util.d.ts +39 -0
  173. package/dist/vendor/types/util.d.ts.map +1 -0
  174. package/dist/vendor/types/util.js +31 -0
  175. package/dist/vendor/types/util.js.map +1 -0
  176. package/package.json +59 -0
package/dist/motion.js ADDED
@@ -0,0 +1,302 @@
1
+ "use client";
2
+ // The React half of DESIGN.md's `## Motion` section. The values live in
3
+ // tokens.ts, which stays importable without React; what needs hooks lives
4
+ // here.
5
+ //
6
+ // THE RULE, BEFORE YOU ADD THE NEXT ANIMATION. An `opacity` below 1 creates a
7
+ // stacking context, and so does any `transform` - mid-flight included, for
8
+ // every frame the transition is running. So both belong on the element being
9
+ // animated, and never on a wrapper that also holds a surface which has to
10
+ // stack independently or stay opaque. Issue #23 was exactly this: the pin
11
+ // marker's wrapper carried `opacity: 0.45` for a resolved pin, the thread it
12
+ // opened was a child of that wrapper, and the thread therefore painted at 45%
13
+ // with the host page's own table rows reading straight through the card - and
14
+ // its `zIndex` resolved inside the wrapper rather than against `layers` in
15
+ // tokens.ts. A transition is the same defect with a timer on it: it is not
16
+ // enough that the resting state is `opacity: 1`, because the 120ms in between
17
+ // composites the subtree just as hard.
18
+ //
19
+ // The three hooks below are deliberately small and deliberately not an
20
+ // animation library. This bundle ships into a customer's application under a
21
+ // gzip ceiling (CLAUDE.md section 8), and everything here is one transition
22
+ // property away from being CSS.
23
+ import { useEffect, useState, useSyncExternalStore, } from "react";
24
+ import { durationMs, exitFor, motion, motionTransform, REDUCED_MOTION_QUERY, transition, } from "./tokens.js";
25
+ /* ── The reviewer's preference ────────────────────────────────────────── */
26
+ function subscribeReducedMotion(listener) {
27
+ if (typeof window.matchMedia !== "function")
28
+ return () => { };
29
+ const query = window.matchMedia(REDUCED_MOTION_QUERY);
30
+ query.addEventListener("change", listener);
31
+ return () => query.removeEventListener("change", listener);
32
+ }
33
+ function prefersReducedMotion() {
34
+ if (typeof window === "undefined" ||
35
+ // A host or a test environment with no `matchMedia` at all. Not
36
+ // hypothetical: jsdom ships without it unless a test adds it, and this
37
+ // hook runs in every surface below.
38
+ typeof window.matchMedia !== "function") {
39
+ return false;
40
+ }
41
+ return window.matchMedia(REDUCED_MOTION_QUERY).matches;
42
+ }
43
+ /**
44
+ * The server's answer, and the hydrating render's: TRUE, no motion.
45
+ *
46
+ * The opposite of the light/dark default beside it, and for a different
47
+ * reason. "Assume light" is a guess at a value; this is a guess at a
48
+ * PERMISSION, and the safe first paint is the one that moves nothing. It is
49
+ * also the other half of DESIGN.md's "nothing animates on first mount": a
50
+ * surface rendered before the media query can be read has no `from` state to
51
+ * animate out of.
52
+ */
53
+ function serverPrefersReducedMotion() {
54
+ return true;
55
+ }
56
+ /**
57
+ * `prefers-reduced-motion: reduce`, read from the host document and kept live.
58
+ *
59
+ * A hook per animating component rather than a field on `ToolbarTheme`. The
60
+ * theme is a colour and elevation object that `themeFor` returns as one of two
61
+ * module constants; widening it to carry a motion preference would make it
62
+ * per-render, which is a real cost for a subscription that is free to
63
+ * duplicate.
64
+ */
65
+ export function usePrefersReducedMotion() {
66
+ return useSyncExternalStore(subscribeReducedMotion, prefersReducedMotion, serverPrefersReducedMotion);
67
+ }
68
+ /* ── Entering ─────────────────────────────────────────────────────────── */
69
+ function noFrameLoop() {
70
+ return (typeof window === "undefined" ||
71
+ typeof window.requestAnimationFrame !== "function");
72
+ }
73
+ /**
74
+ * False on the frame a surface mounts, true on the next one.
75
+ *
76
+ * This is the whole reason an inline `transition` is not enough on its own: a
77
+ * transition never runs on an element's FIRST style computation, so a surface
78
+ * that mounts already at `opacity: 1` has nothing to transition from. The
79
+ * `from` state has to be committed, painted, and only then replaced - which is
80
+ * one `requestAnimationFrame`.
81
+ *
82
+ * `requestAnimationFrame` rather than a layout effect that reads `offsetWidth`
83
+ * to force the same flush. Both work; the reflow version synchronously lays
84
+ * out the HOST page every time a reviewer opens a thread, and this package's
85
+ * standing rule is that it costs the page it is injected into as little as
86
+ * possible. Where there is no frame loop to wait for, the surface simply
87
+ * starts entered - a hard cut is the correct degradation.
88
+ *
89
+ * `settledAtMount` is DESIGN.md's first-mount rule for the one shape that
90
+ * cannot infer it. For a surface that MOUNTS when it opens, the mount is the
91
+ * reviewer's own click and there is nothing to distinguish - it enters, which
92
+ * is the default. For a surface mounted all session and toggled by a prop, an
93
+ * `active` that is already true on the first render is RESTORED STATE, not an
94
+ * action: the inbox sheet's open flag comes out of `localStorage`, so without
95
+ * this the sheet replayed its whole slide-in on every page the reviewer
96
+ * navigated to with it left open - "the toolbar animates itself in on every
97
+ * page load", which is the exact thing DESIGN.md forbids.
98
+ */
99
+ export function useEntered(active = true, settledAtMount = false) {
100
+ const [entered, setEntered] = useState(() => active && (settledAtMount || noFrameLoop()));
101
+ // `active` is for a surface whose component is mounted for the whole
102
+ // session and only shown some of the time - the inbox sheet, which the
103
+ // toolbar renders unconditionally and toggles with a prop. Mount is the
104
+ // wrong moment there: it happens on page load, so by the time a reviewer
105
+ // opens the sheet it has long since "entered" and there is nothing left to
106
+ // animate. Everything else opens by mounting and takes the default.
107
+ //
108
+ // State rather than a ref for the previous value, and not by preference:
109
+ // `react-hooks/refs` is an error in this repo and reading `.current` during
110
+ // render is what it forbids. This is the shape `PinLayer` already uses for
111
+ // its focus request, and React's own documented one for deriving state from
112
+ // a prop that changed.
113
+ const [wasActive, setWasActive] = useState(active);
114
+ if (wasActive !== active) {
115
+ setWasActive(active);
116
+ // Re-armed on every activation: a sheet opened twice enters twice.
117
+ setEntered(noFrameLoop());
118
+ }
119
+ useEffect(() => {
120
+ if (entered || !active)
121
+ return;
122
+ const frame = window.requestAnimationFrame(() => setEntered(true));
123
+ return () => window.cancelAnimationFrame(frame);
124
+ }, [entered, active]);
125
+ return entered;
126
+ }
127
+ /**
128
+ * The two-sided sibling of `useEntered`: false for one frame after `form`
129
+ * changes, true otherwise, and true on the very first render.
130
+ *
131
+ * For a surface that is REPLACED rather than opened. `useEntered` takes a
132
+ * boolean, so it can only re-arm in one direction - `active` going false means
133
+ * "this surface is gone" and its effect deliberately stops there. The bar's
134
+ * two forms (issue #22) are not open and closed: each of them is the current
135
+ * one, and whichever arrives has to enter, so the trigger is a value CHANGING
136
+ * rather than a flag going true.
137
+ *
138
+ * Settled on the first render, and here that is not an option a call site
139
+ * passes but the only correct answer: which form the bar is in comes out of
140
+ * `localStorage`, so a reviewer who collapsed it meets it collapsed on the
141
+ * next page - restored state, not an action - and a toolbar that replays its
142
+ * own collapse on every navigation is precisely what DESIGN.md's first-mount
143
+ * rule forbids. That is the bug `useEntered`'s `settledAtMount` was added for,
144
+ * one surface earlier.
145
+ *
146
+ * THE CALLER MUST REMOUNT THE ELEMENT IT STYLES - key it on the same `form`.
147
+ * Everything else in this file is used by a surface that mounts when it opens,
148
+ * which is why a transition can run on it at all: a transition never runs on
149
+ * an element's FIRST style computation, so the `from` state paints and the
150
+ * `to` state animates. A swap has no mount, so an element reused across it
151
+ * receives `opacity: 0` with the transition ALREADY declared, animates 1 -> 0,
152
+ * and gets reversed one frame later - a hard cut wearing a transition. This
153
+ * hook decides WHEN the from-state is committed; only a new element makes that
154
+ * commit instantaneous. See `BarContents` in FeedbackToolbar.tsx.
155
+ */
156
+ export function useSwapEntered(form) {
157
+ const [entered, setEntered] = useState(true);
158
+ // Derived from a prop that changed, during render: React's own documented
159
+ // shape for this, and the one `react-hooks/refs` leaves available - reading
160
+ // a ref's `.current` during render is an error in this repo. `useEntered`,
161
+ // `usePresence` and `useJustChanged` are all built this way.
162
+ const [seen, setSeen] = useState(form);
163
+ if (seen !== form) {
164
+ setSeen(form);
165
+ // Where there is no frame loop to wait for, the new form is simply already
166
+ // here: a hard cut is the correct degradation, the same one `useEntered`
167
+ // takes.
168
+ setEntered(noFrameLoop());
169
+ }
170
+ useEffect(() => {
171
+ if (entered)
172
+ return;
173
+ const frame = window.requestAnimationFrame(() => setEntered(true));
174
+ return () => window.cancelAnimationFrame(frame);
175
+ }, [entered]);
176
+ return entered;
177
+ }
178
+ /**
179
+ * "Was this one already here when the surface opened?", as a predicate.
180
+ *
181
+ * DESIGN.md's first-mount rule applied to a list: the replies already in a
182
+ * thread are pre-existing state and must not animate, or every reopen of an
183
+ * old thread replays the whole conversation arriving. Only the reply that
184
+ * lands while the reviewer is looking is new.
185
+ *
186
+ * The set is captured on the first render and never updated, which is the
187
+ * point - an id that arrives later stays "new" for as long as this surface is
188
+ * mounted, and the row it belongs to plays its enter exactly once because the
189
+ * row itself mounts exactly once.
190
+ */
191
+ export function useNewSinceMount(ids) {
192
+ // A lazy initialiser rather than a ref filled in on the first render: the
193
+ // set is captured exactly once either way, and `react-hooks/refs` forbids
194
+ // touching `.current` during render.
195
+ const [known] = useState(() => new Set(ids));
196
+ return (id) => !known.has(id);
197
+ }
198
+ /**
199
+ * Keep a surface in the DOM for one exit duration after it closes, then take
200
+ * it out for real.
201
+ *
202
+ * The deferral is DOM REMOVAL ONLY. Whatever state change closed the surface
203
+ * has already been committed by the caller - `openPinId` is null, the sheet's
204
+ * store says closed - so nothing a reviewer did is waiting on an animation,
205
+ * which is DESIGN.md's "an exit never delays the action that triggered it".
206
+ *
207
+ * And the element really does leave. A surface parked at `opacity: 0` forever
208
+ * is markup in a host app's DOM that their own queries and audits trip over,
209
+ * and four e2e specs assert these surfaces reach zero elements - Playwright
210
+ * retries, so a deferred unmount passes and a permanent ghost does not.
211
+ *
212
+ * The latch is set during render rather than in an effect: "a prop changed, so
213
+ * derive some state from it" is React's own documented shape for this, it is
214
+ * what `PinLayer` already does for a focus request, and doing it in an effect
215
+ * would paint one frame of the closed surface still fully open.
216
+ *
217
+ * `exit` is the EXIT duration, and a caller that also drives `surfaceMotion`
218
+ * has to spell it the same way that helper does - `exitFor(<the enter step>)`
219
+ * - or the timer and the transition are two independent literals for the one
220
+ * value DESIGN.md calls "exactly the exit duration".
221
+ */
222
+ export function usePresence(open, exit) {
223
+ const [leaving, setLeaving] = useState(false);
224
+ const [wasOpen, setWasOpen] = useState(open);
225
+ if (wasOpen !== open) {
226
+ setWasOpen(open);
227
+ // Re-opening mid-exit cancels the exit, which is the same statement.
228
+ setLeaving(!open);
229
+ }
230
+ useEffect(() => {
231
+ if (!leaving)
232
+ return;
233
+ const timer = window.setTimeout(() => setLeaving(false), durationMs(exit));
234
+ return () => window.clearTimeout(timer);
235
+ }, [leaving, exit]);
236
+ return { mounted: open || leaving, leaving };
237
+ }
238
+ /**
239
+ * True for one `instant` after `value` changes, and false on first mount.
240
+ *
241
+ * For acknowledging a number that moved rather than a surface that arrived:
242
+ * the unread badge, whose colours and position are identical before and after,
243
+ * so without this a count going 3 → 4 is a glyph swap nobody sees.
244
+ *
245
+ * A latch rather than an enter, and the difference matters: the element has
246
+ * been on screen for a while, so its resting state is already painted and a
247
+ * transition out of it runs immediately - no frame of `from` state to arrange,
248
+ * and therefore no `requestAnimationFrame`.
249
+ */
250
+ export function useJustChanged(value) {
251
+ const [changed, setChanged] = useState(false);
252
+ const [seen, setSeen] = useState(value);
253
+ if (seen !== value) {
254
+ setSeen(value);
255
+ setChanged(true);
256
+ }
257
+ useEffect(() => {
258
+ if (!changed)
259
+ return;
260
+ const timer = window.setTimeout(() => setChanged(false), durationMs(motion.duration.instant));
261
+ return () => window.clearTimeout(timer);
262
+ }, [changed]);
263
+ return changed;
264
+ }
265
+ /**
266
+ * Every entering and leaving surface in this package, styled from one place.
267
+ *
268
+ * A helper rather than five hand-written style fragments because "the same
269
+ * enter everywhere" is the point of having tokens at all, and because the
270
+ * three things easy to get wrong here - which properties are transitioned,
271
+ * which direction the easing runs, what reduced motion drops - are exactly the
272
+ * three a call site would restate slightly differently each time.
273
+ *
274
+ * Only `opacity` and `transform` appear. Both are handed to the compositor and
275
+ * reflow nothing, which is what lets this package animate at all over a host
276
+ * page nobody here wrote.
277
+ */
278
+ export function surfaceMotion({ visible, leaving = false, reduced, origin, slide, duration = motion.duration.fast, }) {
279
+ const shown = visible && !leaving;
280
+ const offset = slide === "x"
281
+ ? `translateX(${motion.enterShift})`
282
+ : slide === "y"
283
+ ? `translateY(${motion.enterShift})`
284
+ : `scale(${motion.enterScale})`;
285
+ return {
286
+ opacity: shown ? 1 : 0,
287
+ // Reduced motion keeps the cross-fade and drops the travel: a surface that
288
+ // has to move is the part of this a vestibular preference is about, and
289
+ // "it appeared" is information rather than decoration. Both halves of that
290
+ // are decided in tokens.ts rather than here - see `motionTransform` and
291
+ // the required `reduced` argument on `transition`.
292
+ transform: motionTransform(shown ? "none" : offset, reduced),
293
+ ...(origin === undefined ? {} : { transformOrigin: origin }),
294
+ transition: transition(["opacity", "transform"],
295
+ // One step faster out than in: arriving is information the reader
296
+ // wants, leaving is a surface getting out of their way. Derived from
297
+ // the enter rather than pinned to the floor, so the rule holds for the
298
+ // slow step as well - see `exitFor`.
299
+ leaving ? exitFor(duration) : duration, leaving ? motion.easing.exit : motion.easing.enter, reduced),
300
+ };
301
+ }
302
+ //# sourceMappingURL=motion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"motion.js","sourceRoot":"","sources":["../src/motion.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ import { type ToolbarDock } from "./tokens.js";
2
+ /**
3
+ * Every toolbar setting that belongs to the reviewer rather than to the build.
4
+ *
5
+ * Deliberately not widened to "anything a component wants to remember": the
6
+ * theme and the inbox flag are not here, and the test for admission is whether
7
+ * a second setting would otherwise want its own key next to this one.
8
+ */
9
+ export interface ToolbarPreferences {
10
+ /** Whether the bar is in its collapsed form. Issue #22. */
11
+ collapsed: boolean;
12
+ /** Which edge or corner the bar is docked to. Issue #21. */
13
+ dock: ToolbarDock;
14
+ }
15
+ export declare const TOOLBAR_PREFERENCES_STORAGE_KEY = "bitvea.feedback-toolbar.preferences";
16
+ /**
17
+ * The answer for a reviewer who has never touched anything, for a host whose
18
+ * storage cannot be read, and for the server.
19
+ *
20
+ * Frozen and shared by identity rather than rebuilt per call, because it is
21
+ * `getServerSnapshot`'s return value and `useSyncExternalStore` compares
22
+ * snapshots with `Object.is` - see the note on `readPreferences`.
23
+ */
24
+ export declare const DEFAULT_PREFERENCES: ToolbarPreferences;
25
+ /**
26
+ * The reviewer's settings, from storage.
27
+ *
28
+ * Memoised on the raw string, and `useSyncExternalStore` is the reason rather
29
+ * than performance: it compares snapshots with `Object.is` and re-renders
30
+ * whenever they differ, so a freshly parsed object on every call is an
31
+ * infinite render loop that React reports as "The result of getSnapshot should
32
+ * be cached to avoid an infinite loop". The two stores this one is modelled on
33
+ * return booleans and never had to think about it; a record has to.
34
+ */
35
+ export declare function readPreferences(): ToolbarPreferences;
36
+ /**
37
+ * Change some of the record, leaving the rest of what is stored alone.
38
+ *
39
+ * Read-merge-write, and the merge is over the STORED keys rather than over
40
+ * `ToolbarPreferences`. This issue writes `collapsed` and #21 writes `dock`;
41
+ * either one writing a whole record built from its own fields would erase the
42
+ * other's the first time a reviewer used both. The same spread is what
43
+ * preserves a key this build has never heard of.
44
+ */
45
+ export declare function writePreferences(patch: Partial<ToolbarPreferences>): void;
46
+ /**
47
+ * The settings as an external store, not as component state read from
48
+ * `localStorage` in a `useState` initialiser.
49
+ *
50
+ * The initialiser is the shape this package already had to fix once, in
51
+ * `INBOX_OPEN_STORE`: on the server the read answers the default and on the
52
+ * client it answers the stored value, so a reviewer who had changed anything
53
+ * got React's "server rendered HTML didn't match the client" in the
54
+ * CUSTOMER's console on every page load. `collapsed` is a harder case than the
55
+ * inbox flag was - it decides which CONTROLS EXIST in the bar - so it takes
56
+ * the same shape: a deterministic server snapshot, and the stored value
57
+ * adopted after hydration rather than during it.
58
+ */
59
+ export declare const PREFERENCES_STORE: {
60
+ subscribe: (listener: () => void) => (() => void);
61
+ /** The client's answer: storage, else whatever this page last chose. */
62
+ getSnapshot: typeof readPreferences;
63
+ /**
64
+ * The server's answer, and the first client render's. The defaults: a bar
65
+ * that hydrates in the form the server drew and then adopts the reviewer's
66
+ * choice is a paint, and one that hydrates into a different set of buttons
67
+ * than the server rendered is an error in somebody else's application.
68
+ */
69
+ getServerSnapshot: () => ToolbarPreferences;
70
+ };
71
+ //# sourceMappingURL=preferences.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preferences.d.ts","sourceRoot":"","sources":["../src/preferences.ts"],"names":[],"mappings":"AAuCA,OAAO,EAA+B,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzE;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,2DAA2D;IAC3D,SAAS,EAAE,OAAO,CAAC;IACnB,4DAA4D;IAC5D,IAAI,EAAE,WAAW,CAAC;CACnB;AAED,eAAO,MAAM,+BAA+B,wCACL,CAAC;AAExC;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,EAAE,kBAGhC,CAAC;AA2EH;;;;;;;;;GASG;AACH,wBAAgB,eAAe,IAAI,kBAAkB,CAOpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,kBAAkB,CAAC,GAAG,IAAI,CASzE;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iBAAiB;0BAIN,MAAM,IAAI,KAAG,CAAC,MAAM,IAAI,CAAC;IAM/C,wEAAwE;;IAExE;;;;;OAKG;6BACoB,kBAAkB;CAC1C,CAAC"}
@@ -0,0 +1,195 @@
1
+ // What this reviewer did to the toolbar itself, as ONE record under ONE key.
2
+ //
3
+ // Two things about the bar are the reviewer's to decide and have to survive a
4
+ // reload: whether it is collapsed (issue #22) and, next, where it is docked
5
+ // (issue #21). Issue #22 says in as many words that those are "one persisted
6
+ // preference object, not two competing ones", and the reason is not tidiness.
7
+ // Two keys written by two independent toggles are two things that can disagree
8
+ // about what the bar looks like, and the second one added is always the one
9
+ // that forgets the first exists. So this is a RECORD from the first field, and
10
+ // #21 landed exactly the way that promised: one more property on
11
+ // `ToolbarPreferences`, one more line in `parsePreferences`, one more key in
12
+ // the frozen defaults, and the import of the guard that validates it. The
13
+ // read-merge-write and the raw-string memoisation below did not have to move.
14
+ //
15
+ // The two single-valued preferences already in this package stay where they
16
+ // are: `THEME_STORAGE_KEY` in tokens.ts and `INBOX_OPEN_STORAGE_KEY` in
17
+ // InboxSheet.tsx, each read by the store that owns it and each named by a
18
+ // unit test. Neither is what #22 is about, so this file is a new home for new
19
+ // settings rather than a migration - and the key below is deliberately NOT on
20
+ // the package's public surface either, unlike `INBOX_OPEN_STORAGE_KEY`, which
21
+ // index.ts exports because a host replacing the inbox has to be able to read
22
+ // the flag the default one wrote.
23
+ //
24
+ // STORAGE HERE BELONGS TO THE HOST, and all three consequences of that are
25
+ // real failure modes rather than hypotheticals:
26
+ //
27
+ // - The accessor itself throws. Safari's private mode and a blocked origin
28
+ // throw on `window.localStorage`, not on `getItem`, so every access sits
29
+ // inside a `try`. An exception out of this file takes the host
30
+ // application's render down with it.
31
+ // - The key sits in the host's own key space, beside whatever they store, so
32
+ // it is namespaced `bitvea.feedback-toolbar.*` like its two neighbours.
33
+ // - The VALUE can be anything. It is the host's origin: their code, a
34
+ // browser extension, or an older or newer build of this toolbar can have
35
+ // written it. So parsing is defensive per field, and unknown keys are
36
+ // preserved on write - which is what lets a browser that has already
37
+ // stored #21's dock keep it through a downgrade to a build that has never
38
+ // heard of one.
39
+ import { DEFAULT_DOCK, isToolbarDock } from "./tokens.js";
40
+ export const TOOLBAR_PREFERENCES_STORAGE_KEY = "bitvea.feedback-toolbar.preferences";
41
+ /**
42
+ * The answer for a reviewer who has never touched anything, for a host whose
43
+ * storage cannot be read, and for the server.
44
+ *
45
+ * Frozen and shared by identity rather than rebuilt per call, because it is
46
+ * `getServerSnapshot`'s return value and `useSyncExternalStore` compares
47
+ * snapshots with `Object.is` - see the note on `readPreferences`.
48
+ */
49
+ export const DEFAULT_PREFERENCES = Object.freeze({
50
+ collapsed: false,
51
+ dock: DEFAULT_DOCK,
52
+ });
53
+ const listeners = new Set();
54
+ /**
55
+ * The record as it was last written, when it could not be stored - private
56
+ * mode, a blocked origin, a host that disabled storage, a full quota. The
57
+ * choice then holds for this page and no longer, which beats a control that
58
+ * appears not to work at all.
59
+ */
60
+ let memoryRaw = null;
61
+ /** The last string parsed, and what it parsed to. See `readPreferences`. */
62
+ let cachedRaw = null;
63
+ let cached = DEFAULT_PREFERENCES;
64
+ function readRaw() {
65
+ // Memory FIRST, which is one step further than the theme and inbox stores go
66
+ // - they consult their fallback only when the read throws. A write is what
67
+ // fails on a full quota or a partially blocked origin: `getItem` keeps
68
+ // working there and keeps answering the value from before the reviewer's
69
+ // click, so a storage-first read would honour a preference they had just
70
+ // replaced. `memoryRaw` is non-null only because a write could not be
71
+ // stored, and in that case it is the truth for this page.
72
+ if (memoryRaw !== null)
73
+ return memoryRaw;
74
+ try {
75
+ return window.localStorage.getItem(TOOLBAR_PREFERENCES_STORAGE_KEY);
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
81
+ /**
82
+ * The stored JSON as a plain object, with every key it happened to carry, or
83
+ * null for anything that is not one - malformed JSON, a string, a number, an
84
+ * array. `Array.isArray` is checked separately because an array IS a
85
+ * `typeof "object"` and spreading one produces index keys.
86
+ */
87
+ function parseObject(raw) {
88
+ if (raw === null)
89
+ return null;
90
+ try {
91
+ const value = JSON.parse(raw);
92
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
93
+ return null;
94
+ }
95
+ return value;
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
101
+ /**
102
+ * Per FIELD, and that is the whole of the parsing policy: a stored record with
103
+ * `collapsed: "yes"` in it falls back to the default for `collapsed` and keeps
104
+ * whatever else it holds, rather than being thrown away wholesale. With two
105
+ * fields written by two different issues, discarding the record on one bad
106
+ * value means one bad write erases the other feature's setting.
107
+ */
108
+ function parsePreferences(raw) {
109
+ const stored = parseObject(raw);
110
+ if (stored === null)
111
+ return DEFAULT_PREFERENCES;
112
+ return {
113
+ collapsed: typeof stored.collapsed === "boolean"
114
+ ? stored.collapsed
115
+ : DEFAULT_PREFERENCES.collapsed,
116
+ // Through the same guard `isThemeMode` is: a dock this build has never
117
+ // heard of - a newer one's, or a browser extension's - reads back as the
118
+ // default rather than as a value the layout table has no row for. The
119
+ // stored string itself survives, because the write below merges over the
120
+ // STORED keys and never over this record.
121
+ dock: isToolbarDock(stored.dock) ? stored.dock : DEFAULT_PREFERENCES.dock,
122
+ };
123
+ }
124
+ /**
125
+ * The reviewer's settings, from storage.
126
+ *
127
+ * Memoised on the raw string, and `useSyncExternalStore` is the reason rather
128
+ * than performance: it compares snapshots with `Object.is` and re-renders
129
+ * whenever they differ, so a freshly parsed object on every call is an
130
+ * infinite render loop that React reports as "The result of getSnapshot should
131
+ * be cached to avoid an infinite loop". The two stores this one is modelled on
132
+ * return booleans and never had to think about it; a record has to.
133
+ */
134
+ export function readPreferences() {
135
+ const raw = readRaw();
136
+ if (raw !== cachedRaw) {
137
+ cachedRaw = raw;
138
+ cached = parsePreferences(raw);
139
+ }
140
+ return cached;
141
+ }
142
+ /**
143
+ * Change some of the record, leaving the rest of what is stored alone.
144
+ *
145
+ * Read-merge-write, and the merge is over the STORED keys rather than over
146
+ * `ToolbarPreferences`. This issue writes `collapsed` and #21 writes `dock`;
147
+ * either one writing a whole record built from its own fields would erase the
148
+ * other's the first time a reviewer used both. The same spread is what
149
+ * preserves a key this build has never heard of.
150
+ */
151
+ export function writePreferences(patch) {
152
+ const raw = JSON.stringify({ ...(parseObject(readRaw()) ?? {}), ...patch });
153
+ try {
154
+ window.localStorage.setItem(TOOLBAR_PREFERENCES_STORAGE_KEY, raw);
155
+ memoryRaw = null;
156
+ }
157
+ catch {
158
+ memoryRaw = raw;
159
+ }
160
+ listeners.forEach((listener) => listener());
161
+ }
162
+ /**
163
+ * The settings as an external store, not as component state read from
164
+ * `localStorage` in a `useState` initialiser.
165
+ *
166
+ * The initialiser is the shape this package already had to fix once, in
167
+ * `INBOX_OPEN_STORE`: on the server the read answers the default and on the
168
+ * client it answers the stored value, so a reviewer who had changed anything
169
+ * got React's "server rendered HTML didn't match the client" in the
170
+ * CUSTOMER's console on every page load. `collapsed` is a harder case than the
171
+ * inbox flag was - it decides which CONTROLS EXIST in the bar - so it takes
172
+ * the same shape: a deterministic server snapshot, and the stored value
173
+ * adopted after hydration rather than during it.
174
+ */
175
+ export const PREFERENCES_STORE = {
176
+ // Arrow properties rather than shorthand methods: these are handed to
177
+ // `useSyncExternalStore` detached from the object, so nothing here may
178
+ // depend on `this`.
179
+ subscribe: (listener) => {
180
+ listeners.add(listener);
181
+ return () => {
182
+ listeners.delete(listener);
183
+ };
184
+ },
185
+ /** The client's answer: storage, else whatever this page last chose. */
186
+ getSnapshot: readPreferences,
187
+ /**
188
+ * The server's answer, and the first client render's. The defaults: a bar
189
+ * that hydrates in the form the server drew and then adopts the reviewer's
190
+ * choice is a paint, and one that hydrates into a different set of buttons
191
+ * than the server rendered is an error in somebody else's application.
192
+ */
193
+ getServerSnapshot: () => DEFAULT_PREFERENCES,
194
+ };
195
+ //# sourceMappingURL=preferences.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preferences.js","sourceRoot":"","sources":["../src/preferences.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Rasterise the visible viewport to a PNG data URL, or null if the browser
3
+ * would not produce one.
4
+ *
5
+ * @param maxDataUrlLength Longest data URL the API will accept. The image is
6
+ * re-encoded at half scale, twice, before giving up, so a very large or very
7
+ * dense viewport degrades in quality rather than disappearing.
8
+ */
9
+ export declare function rasteriseViewport(maxDataUrlLength: number): Promise<string | null>;
10
+ /**
11
+ * Rasterise ONE element to a PNG data URL, or null if the browser would not
12
+ * produce one.
13
+ *
14
+ * A reviewer pointing at a broken button wants a picture of the button, not of
15
+ * the page it is on: the whole-viewport shot puts the thing they are reporting
16
+ * somewhere in a 1512x916 image and leaves the reader to find it. This is the
17
+ * camera in the composer.
18
+ *
19
+ * An element with no box - `display: none`, or detached - yields null rather
20
+ * than a 1x1 image, because a blank attachment is worse than none.
21
+ */
22
+ export declare function rasteriseElement(element: Element, maxDataUrlLength: number): Promise<string | null>;
23
+ /**
24
+ * Serialise the visible page as an SVG document string, or null if the page is
25
+ * too large to walk. Exported for the unit tests: it is pure DOM work with no
26
+ * canvas or image decoding in it, so it is the part that is provable in jsdom.
27
+ */
28
+ export declare function buildViewportSvg(width: number, height: number,
29
+ /**
30
+ * Where in the viewport the picture starts. `{x: 0, y: 0}` is the viewport
31
+ * shot; an element shot passes its own top-left, and the body clone shifts
32
+ * by the same amount so the element lands at the picture's origin.
33
+ *
34
+ * Cropping by MOVING THE WHOLE PAGE rather than by cloning only the element
35
+ * is the decision here. An element does not carry its own layout: its width
36
+ * may come from a flex parent, its colour from a `.dark` ancestor, its font
37
+ * from `html`. Cloning it alone reproduces none of that, and the picture
38
+ * that comes back is not the thing the reviewer pointed at. The page is
39
+ * cloned exactly as it is and the viewport is narrowed instead.
40
+ */
41
+ origin?: {
42
+ x: number;
43
+ y: number;
44
+ }): string | null;
45
+ //# sourceMappingURL=rasterise.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rasterise.d.ts","sourceRoot":"","sources":["../src/rasterise.ts"],"names":[],"mappings":"AAgNA;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAExB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,OAAO,EAChB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAIxB;AAsDD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM;AACd;;;;;;;;;;;GAWG;AACH,MAAM,GAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAmB,GAChD,MAAM,GAAG,IAAI,CAyCf"}