@box-kite/react 2.2.7 → 2.2.9

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 (96) hide show
  1. package/BOX_AI_CONTEXT.md +76 -0
  2. package/BOX_KITE_AI_CONTEXT.md +76 -0
  3. package/analytics.cjs +1 -0
  4. package/analytics.d.ts +10 -0
  5. package/analytics.mjs +121 -0
  6. package/behavior.cjs +1 -1
  7. package/behavior.mjs +42 -38
  8. package/box.cjs +1 -1
  9. package/box.d.ts +9 -0
  10. package/box.mjs +17 -16
  11. package/catalog.cjs +1 -1
  12. package/catalog.mjs +8 -0
  13. package/client.cjs +1 -1
  14. package/client.mjs +120 -120
  15. package/components/accordion.cjs +1 -1
  16. package/components/accordion.mjs +19 -10
  17. package/components/agent.cjs +1 -1
  18. package/components/agent.mjs +21 -8
  19. package/components/calendar.cjs +1 -1
  20. package/components/calendar.d.ts +3 -1
  21. package/components/calendar.mjs +154 -142
  22. package/components/combobox.cjs +1 -1
  23. package/components/combobox.mjs +184 -174
  24. package/components/dashboard.cjs +1 -1
  25. package/components/dashboard.mjs +183 -170
  26. package/components/dataGrid/models/columnModel.d.ts +1 -0
  27. package/components/dataGrid/models/gridModel.d.ts +18 -0
  28. package/components/dataGrid/models/rangeModel.d.ts +3 -0
  29. package/components/dataGrid.cjs +3 -3
  30. package/components/dataGrid.mjs +987 -896
  31. package/components/dateField.cjs +1 -1
  32. package/components/dateField.mjs +160 -149
  33. package/components/datePicker.cjs +1 -1
  34. package/components/datePicker.mjs +226 -206
  35. package/components/dialog.cjs +1 -1
  36. package/components/dialog.mjs +38 -33
  37. package/components/dropdown.cjs +1 -1
  38. package/components/dropdown.mjs +107 -93
  39. package/components/form.cjs +1 -1
  40. package/components/form.mjs +22 -18
  41. package/components/menu.cjs +1 -1
  42. package/components/menu.d.ts +2 -0
  43. package/components/menu.mjs +196 -175
  44. package/components/overlay.cjs +1 -1
  45. package/components/overlay.mjs +7 -7
  46. package/components/popover.cjs +1 -1
  47. package/components/popover.mjs +4 -0
  48. package/components/radioGroup.cjs +1 -1
  49. package/components/radioGroup.mjs +10 -5
  50. package/components/slider.cjs +1 -1
  51. package/components/slider.mjs +84 -83
  52. package/components/sortableList.cjs +1 -1
  53. package/components/sortableList.mjs +158 -150
  54. package/components/swipeItem.cjs +1 -1
  55. package/components/swipeItem.mjs +92 -84
  56. package/components/tabs.cjs +1 -1
  57. package/components/tabs.mjs +10 -5
  58. package/components/timeZoneSelect.cjs +1 -1
  59. package/components/timeZoneSelect.mjs +40 -35
  60. package/components/toaster.cjs +1 -1
  61. package/components/toaster.mjs +151 -113
  62. package/dataGridDateParts.cjs +1 -1
  63. package/dataGridDateParts.mjs +56 -55
  64. package/dataGridList.cjs +1 -1
  65. package/dataGridList.mjs +430 -429
  66. package/effects.cjs +1 -1
  67. package/effects.mjs +1 -1
  68. package/interactionListening.cjs +1 -0
  69. package/interactionListening.mjs +117 -0
  70. package/interactions.cjs +1 -0
  71. package/interactions.mjs +25 -0
  72. package/layer.cjs +1 -1
  73. package/layer.mjs +68 -66
  74. package/package.json +132 -68
  75. package/placement.cjs +1 -1
  76. package/placement.mjs +10 -10
  77. package/platform.cjs +1 -1
  78. package/platform.mjs +44 -25
  79. package/react/interactions/interactions.d.ts +30 -0
  80. package/react/popover/usePopoverLayer.d.ts +3 -0
  81. package/react/useStyles.d.ts +8 -1
  82. package/react-shared.cjs +1 -1
  83. package/react-shared.mjs +49 -48
  84. package/reporting.cjs +1 -0
  85. package/reporting.mjs +51 -0
  86. package/scrolling.cjs +1 -1
  87. package/scrolling.mjs +1 -1
  88. package/supabase/functions/collect/index.d.ts +30 -0
  89. package/supabase/functions/collect/index.test.d.ts +1 -0
  90. package/utils/analytics/analytics.d.ts +72 -0
  91. package/utils/analytics/analyticsIdentity.d.ts +13 -0
  92. package/utils/interactions/componentAttribute.d.ts +6 -0
  93. package/utils/interactions/interactionChannel.d.ts +35 -0
  94. package/utils/interactions/interactionJson.d.ts +6 -0
  95. package/utils/interactions/interactionListening.d.ts +6 -0
  96. package/utils/interactions/interactionTypes.d.ts +224 -0
package/BOX_AI_CONTEXT.md CHANGED
@@ -3936,3 +3936,79 @@ call being shown and `renderAndWaitForResponse` is a question whose `respond` is
3936
3936
  `onDecisionChange` calls. json-render is the same `z.fromJSONSchema` over `catalog()` itself.
3937
3937
 
3938
3938
  Full reference: `docs/interop.md`.
3939
+
3940
+ ## Interaction events (`Box.onInteraction`)
3941
+
3942
+ One listener hears every interaction, in one typed stream, so an analytics call, an audit log or a session recorder is one function:
3943
+
3944
+ ```tsx
3945
+ const stop = Box.onInteraction((event) => {
3946
+ analytics.track(`${event.component}.${event.type}`, {
3947
+ kind: event.kind,
3948
+ source: event.source,
3949
+ reason: event.reason,
3950
+ value: event.value,
3951
+ at: event.time,
3952
+ });
3953
+ });
3954
+ ```
3955
+
3956
+ - **`kind`** is who reported it. `'change'` is a component's own change: `type` is the callback it mirrors without its `on`, `reason` is the word that callback is given (`'escape'`, `'outside-pointer'`, `'keyboard'`…), `value` the new value and `details` the rest of what the callback is told. `'dom'` is a `click` or a `change` on anything a component styled, named by the nearest `data-bk` (below). `'custom'` is the app's own, from `Box.reportInteraction`.
3957
+ - **The stream is typed**: narrow on `kind`, then `component` and `type`, and `value` is what that change carries — `if (e.kind === 'change' && e.component === 'datagrid' && e.type === 'sortingChange') e.value?.columnKey`. The whole map is `ComponentInteractions`, exported from `@box-kite/react` with `InteractionEvent`.
3958
+ - **`value` and `details` are plain JSON copies**, so an event goes to a log or a beacon as it stands: a `Date` is its ISO string, a `Set` an array, and a function, a React element, a DOM node or a class instance is left out.
3959
+ - **What a person typed is content, and a listener asks for it.** A DataGrid search, filter values, a cell's value before and after an edit (and a refusal's message), the cells a paste wrote, a date picked or typed, the rows a `Combobox` selected and a toast's text are left out of what a listener is handed, and `url` has no query string and no hash. `Box.onInteraction(listener, { content: true })` hands all of it over. A report names its content in `content: ['value', 'oldValue']` (the value and any `details` keys), which is how `Box.reportInteraction` marks an app's own.
3960
+ - **`source`** is `'app'` when code did it — the reasons `imperative`, `programmatic` and `timeout`, a `toast()` call, an untrusted DOM event — and `'user'` otherwise.
3961
+ - **`event` is the DOM event behind it** — passed by the component, or the one the browser was dispatching when the change happened, so a DataGrid sorted by a header press carries that press. **`target`** is the control that handled it (the tab, not the icon inside it), else the component's own element. `timeStamp` is `performance.now()`, `time` is `Date.now()` and `url` is the page it happened on, so an event delivered after a navigation still names its own.
3962
+ - **It fires whether or not the app passed the callback, and it is fire and forget.** An event is queued as it happens and delivered in order, batched, in a **background task** after the handler that caused it (`scheduler.postTask` at `background` priority, `setTimeout` where there is none), so a slow listener cannot delay the click, the render or the paint it reports, and a listener that throws goes to `reportError` while the rest still run. `pagehide` delivers whatever is still queued, and **`Box.flushInteractions()`** delivers it now, which is what an analytics sender calls before it sends its last batch, and what a test calls before it asserts.
3963
+ - **With nobody listening, a report costs one comparison** and nothing is kept, so subscribe in the module that boots the app: what happens before the first listener is gone. The delivery itself (the batching, the JSON copies, the press listeners) is a chunk of its own, fetched with the first subscription; anything reported while it is on its way waits for it.
3964
+ - **One choice is one event, from the component the app rendered.** A component built from others keeps its parts quiet: a day picked in a `DatePicker` is `datePicker.valueChange` and `datePicker.openChange` (`select`), never `calendar.valueChange` as well. The same holds for the `Combobox` in a `TimeZoneSelect` and for the DataGrid's own filters, editors and panels, whose changes the grid reports itself — while a column's `Cell` and a `rowDetail`, which are the app's, report as usual wherever the grid renders them.
3965
+ - **On a server nothing listens** — no person is interacting there, and a subscription there is a no-op — and the channel lives on `globalThis` under `Symbol.for('box-kite.interactions@2')`, so two copies of the package share it.
3966
+
3967
+ | `component` | `type` (and `reason`) |
3968
+ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
3969
+ | `dialog`, `alertDialog`, `popover`, `menu`, `datePicker`, `combobox`, `dropdown`, `timeZoneSelect`, `collapsible`, `toolCall`, `reasoning`, `swipeItem` | `openChange` · `menu.sub` `openChange` only when the keyboard opens it, with `details.item` |
3970
+ | `tabs`, `accordion`, `radioGroup`, `dropdown`, `combobox`, `calendar`, `dateField`, `datePicker`, `dateRangePicker`, `timeZoneSelect`, `menu.radioGroup` | `valueChange` |
3971
+ | `menu.item` | `select` — the value is the item's `value` prop, else its text · `menu.checkboxItem` `checkedChange`, with `details.item` · `approval` `decisionChange` |
3972
+ | `calendar` | `pageChange` (`next`/`previous`), `viewChange` (`zoom-in`/`zoom-out`) |
3973
+ | `slider` | `valueCommit` · `dashboard` `layoutCommit`, `grab`, `grabCancel` · `widget` `refresh` (`refresh`/`retry`) · `sortableList` `orderChange`, `orderCommit`, `grab`, `grabCancel` · `swipeItem` `fullSwipe` |
3974
+ | `form` | `submit` — the value is the **field names**, never what was typed into them |
3975
+ | `toaster` | `show` (reason: the kind), `update`, `dismiss` (`timeout`/`close`/`action`/`imperative`), `action` (an action that leaves the toast up) |
3976
+ | `datagrid` | `sortingChange`, `globalFilterChange`, `columnFiltersChange`, `paginationChange`, `selectedRowKeysChange`, `expandedRowKeysChange`, `expandedTreeKeysChange`, `rangeChange` (a marked block only), `copy`, `cellEditStart`, `cellEdit`, `cellEditCancel`, `cellEditReject`, `paste`, `columnPinChange`, `columnVisibilityChange`, `columnResize`, `groupingChange`, `groupExpandedChange`, `selectionViewChange`, `export`, `retry` — and on a phone `recordChange`, `sheetChange`, `selectModeChange`, `showMore` |
3977
+
3978
+ **Nothing reports per frame, per hover or per keystroke.** A drag reports where it ends (`valueCommit`, `layoutCommit`, `columnResize`, a range block), so a slider's `valueChange` and a dashboard's `layoutChange` are not events here; neither is a tooltip shown on hover, a submenu opened by the pointer, the text typed into a combobox or a dropdown's search, focus moving in a calendar or a grid, nor a page reset that a sort or a filter caused. A change the app makes through a ref (`exportCsv()`, `clearEdits()`, `refresh()`) does not report; one that goes through a component's state (closing a `Dialog` from code) reports with `source: 'app'`.
3979
+
3980
+ **A press is a `dom` event, and it needs `data-bk`.** `Box.configure({ componentAttribute: true })` writes `data-bk="<component key>"` on every element a component styles, user components from `Box.components()` included, and the channel names a `click` or a `change` by the nearest one — on the control it hit, with `details.interactive` saying whether a control was hit at all, so a dead click is countable. A press that changed something arrives as that change, never twice: `tabs.valueChange` carries the click as its `event` and the tab as its `target`, and so does an app's own `Box.reportInteraction` made inside the handler. Only a press nothing explained — a plain `Button`, a dead click — is a `dom` event. Without the attribute nothing can be named, and development says so once in the console.
3981
+
3982
+ ```tsx
3983
+ Box.configure({ componentAttribute: true }); // in a module both the server and the browser run
3984
+ ```
3985
+
3986
+ An app's own `data-bk` in `props` wins. Set the flag before the first render, on both sides, or a hydrating browser sees attributes the server never wrote. The half of the channel that delivers and watches the page is a chunk of under 1.5 KB, fetched with the first subscription; in a test, `await vi.dynamicImportSettled()` after subscribing, before the first assertion.
3987
+
3988
+ **A moment of the app's own is `Box.reportInteraction`.** Box never wraps the handlers an app passes in `props`: wrapping them all would cost every render, report a hover or a scroll per frame, and name nothing better than `button`. So a hover, a section scrolled into view or a wizard step is reported where it happens, in the same shape, to the same listeners, fire and forget:
3989
+
3990
+ ```tsx
3991
+ <Button props={{ onMouseEnter: (event) => Box.reportInteraction({ component: 'pricing-cta', type: 'mouseEnter', event }) }}>Pricing</Button>
3992
+ ```
3993
+
3994
+ It is a plain function, not a hook, so it works in an effect, a fetch callback or a module with no React in it. `event` may be React's synthetic one; `target`, `source`, `timeStamp`, `time` and `url` are filled in. Keep it to discrete moments, the rule the components follow.
3995
+
3996
+ ### Sending it somewhere: `@box-kite/react/analytics`
3997
+
3998
+ `createAnalytics({ send })` is that stream collected: every interaction as a flat JSON record with a session id, batched, held until consent is granted, and sent as the page is hidden. No React and no engine, 1.9 KB gzipped.
3999
+
4000
+ ```ts
4001
+ import { beacon, consoleLog, createAnalytics } from '@box-kite/react/analytics';
4002
+
4003
+ const analytics = createAnalytics({
4004
+ send: [beacon('https://collect.example.com/events'), consoleLog()],
4005
+ consent: 'pending', // nothing stored or sent until the reader answers
4006
+ });
4007
+ analytics.consent('granted');
4008
+ ```
4009
+
4010
+ - **A record** is `{ id, name, kind, component, type, reason, value, details, source, trigger, label, time, url, sessionId, visitorId? }`: `name` is `component.type`, `trigger` the DOM event's type, `label` the words on a pressed button, link, tab or menu item (never a cell's or a field's without `content`), `time` ISO 8601 and `id` unique, so a collector sent a batch twice keeps it once.
4011
+ - **`sessionId` is one visit**, in `sessionStorage`: it survives a reload, ends with the tab, and starts again after `sessionTimeout` (30 minutes) with nothing happening. **`visitor: true`** adds a `visitorId` in `localStorage`, one browser across visits, written only once consent is granted.
4012
+ - **`consent`**: `'granted'` (default), `'pending'` (held in memory, nothing stored or sent) or `'denied'` (dropped, both ids forgotten); `analytics.consent(next)` answers it later.
4013
+ - **`content: true`** passes the stream's own option through. **`batch: { size, wait }`** (20, 5000 ms), **`filter(event)`** on the raw interaction, **`analytics.flush()`** and **`analytics.stop()`**.
4014
+ - **A transport is a function of the batch.** `beacon(url)` posts `{ "events": [...] }` with `sendBeacon` as `text/plain` (no preflight; read the body as JSON), falling back to a `keepalive` fetch. A vendor is one more: `send: (events) => events.forEach((e) => posthog.capture(e.name, e))`.
@@ -3936,3 +3936,79 @@ call being shown and `renderAndWaitForResponse` is a question whose `respond` is
3936
3936
  `onDecisionChange` calls. json-render is the same `z.fromJSONSchema` over `catalog()` itself.
3937
3937
 
3938
3938
  Full reference: `docs/interop.md`.
3939
+
3940
+ ## Interaction events (`Box.onInteraction`)
3941
+
3942
+ One listener hears every interaction, in one typed stream, so an analytics call, an audit log or a session recorder is one function:
3943
+
3944
+ ```tsx
3945
+ const stop = Box.onInteraction((event) => {
3946
+ analytics.track(`${event.component}.${event.type}`, {
3947
+ kind: event.kind,
3948
+ source: event.source,
3949
+ reason: event.reason,
3950
+ value: event.value,
3951
+ at: event.time,
3952
+ });
3953
+ });
3954
+ ```
3955
+
3956
+ - **`kind`** is who reported it. `'change'` is a component's own change: `type` is the callback it mirrors without its `on`, `reason` is the word that callback is given (`'escape'`, `'outside-pointer'`, `'keyboard'`…), `value` the new value and `details` the rest of what the callback is told. `'dom'` is a `click` or a `change` on anything a component styled, named by the nearest `data-bk` (below). `'custom'` is the app's own, from `Box.reportInteraction`.
3957
+ - **The stream is typed**: narrow on `kind`, then `component` and `type`, and `value` is what that change carries — `if (e.kind === 'change' && e.component === 'datagrid' && e.type === 'sortingChange') e.value?.columnKey`. The whole map is `ComponentInteractions`, exported from `@box-kite/react` with `InteractionEvent`.
3958
+ - **`value` and `details` are plain JSON copies**, so an event goes to a log or a beacon as it stands: a `Date` is its ISO string, a `Set` an array, and a function, a React element, a DOM node or a class instance is left out.
3959
+ - **What a person typed is content, and a listener asks for it.** A DataGrid search, filter values, a cell's value before and after an edit (and a refusal's message), the cells a paste wrote, a date picked or typed, the rows a `Combobox` selected and a toast's text are left out of what a listener is handed, and `url` has no query string and no hash. `Box.onInteraction(listener, { content: true })` hands all of it over. A report names its content in `content: ['value', 'oldValue']` (the value and any `details` keys), which is how `Box.reportInteraction` marks an app's own.
3960
+ - **`source`** is `'app'` when code did it — the reasons `imperative`, `programmatic` and `timeout`, a `toast()` call, an untrusted DOM event — and `'user'` otherwise.
3961
+ - **`event` is the DOM event behind it** — passed by the component, or the one the browser was dispatching when the change happened, so a DataGrid sorted by a header press carries that press. **`target`** is the control that handled it (the tab, not the icon inside it), else the component's own element. `timeStamp` is `performance.now()`, `time` is `Date.now()` and `url` is the page it happened on, so an event delivered after a navigation still names its own.
3962
+ - **It fires whether or not the app passed the callback, and it is fire and forget.** An event is queued as it happens and delivered in order, batched, in a **background task** after the handler that caused it (`scheduler.postTask` at `background` priority, `setTimeout` where there is none), so a slow listener cannot delay the click, the render or the paint it reports, and a listener that throws goes to `reportError` while the rest still run. `pagehide` delivers whatever is still queued, and **`Box.flushInteractions()`** delivers it now, which is what an analytics sender calls before it sends its last batch, and what a test calls before it asserts.
3963
+ - **With nobody listening, a report costs one comparison** and nothing is kept, so subscribe in the module that boots the app: what happens before the first listener is gone. The delivery itself (the batching, the JSON copies, the press listeners) is a chunk of its own, fetched with the first subscription; anything reported while it is on its way waits for it.
3964
+ - **One choice is one event, from the component the app rendered.** A component built from others keeps its parts quiet: a day picked in a `DatePicker` is `datePicker.valueChange` and `datePicker.openChange` (`select`), never `calendar.valueChange` as well. The same holds for the `Combobox` in a `TimeZoneSelect` and for the DataGrid's own filters, editors and panels, whose changes the grid reports itself — while a column's `Cell` and a `rowDetail`, which are the app's, report as usual wherever the grid renders them.
3965
+ - **On a server nothing listens** — no person is interacting there, and a subscription there is a no-op — and the channel lives on `globalThis` under `Symbol.for('box-kite.interactions@2')`, so two copies of the package share it.
3966
+
3967
+ | `component` | `type` (and `reason`) |
3968
+ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
3969
+ | `dialog`, `alertDialog`, `popover`, `menu`, `datePicker`, `combobox`, `dropdown`, `timeZoneSelect`, `collapsible`, `toolCall`, `reasoning`, `swipeItem` | `openChange` · `menu.sub` `openChange` only when the keyboard opens it, with `details.item` |
3970
+ | `tabs`, `accordion`, `radioGroup`, `dropdown`, `combobox`, `calendar`, `dateField`, `datePicker`, `dateRangePicker`, `timeZoneSelect`, `menu.radioGroup` | `valueChange` |
3971
+ | `menu.item` | `select` — the value is the item's `value` prop, else its text · `menu.checkboxItem` `checkedChange`, with `details.item` · `approval` `decisionChange` |
3972
+ | `calendar` | `pageChange` (`next`/`previous`), `viewChange` (`zoom-in`/`zoom-out`) |
3973
+ | `slider` | `valueCommit` · `dashboard` `layoutCommit`, `grab`, `grabCancel` · `widget` `refresh` (`refresh`/`retry`) · `sortableList` `orderChange`, `orderCommit`, `grab`, `grabCancel` · `swipeItem` `fullSwipe` |
3974
+ | `form` | `submit` — the value is the **field names**, never what was typed into them |
3975
+ | `toaster` | `show` (reason: the kind), `update`, `dismiss` (`timeout`/`close`/`action`/`imperative`), `action` (an action that leaves the toast up) |
3976
+ | `datagrid` | `sortingChange`, `globalFilterChange`, `columnFiltersChange`, `paginationChange`, `selectedRowKeysChange`, `expandedRowKeysChange`, `expandedTreeKeysChange`, `rangeChange` (a marked block only), `copy`, `cellEditStart`, `cellEdit`, `cellEditCancel`, `cellEditReject`, `paste`, `columnPinChange`, `columnVisibilityChange`, `columnResize`, `groupingChange`, `groupExpandedChange`, `selectionViewChange`, `export`, `retry` — and on a phone `recordChange`, `sheetChange`, `selectModeChange`, `showMore` |
3977
+
3978
+ **Nothing reports per frame, per hover or per keystroke.** A drag reports where it ends (`valueCommit`, `layoutCommit`, `columnResize`, a range block), so a slider's `valueChange` and a dashboard's `layoutChange` are not events here; neither is a tooltip shown on hover, a submenu opened by the pointer, the text typed into a combobox or a dropdown's search, focus moving in a calendar or a grid, nor a page reset that a sort or a filter caused. A change the app makes through a ref (`exportCsv()`, `clearEdits()`, `refresh()`) does not report; one that goes through a component's state (closing a `Dialog` from code) reports with `source: 'app'`.
3979
+
3980
+ **A press is a `dom` event, and it needs `data-bk`.** `Box.configure({ componentAttribute: true })` writes `data-bk="<component key>"` on every element a component styles, user components from `Box.components()` included, and the channel names a `click` or a `change` by the nearest one — on the control it hit, with `details.interactive` saying whether a control was hit at all, so a dead click is countable. A press that changed something arrives as that change, never twice: `tabs.valueChange` carries the click as its `event` and the tab as its `target`, and so does an app's own `Box.reportInteraction` made inside the handler. Only a press nothing explained — a plain `Button`, a dead click — is a `dom` event. Without the attribute nothing can be named, and development says so once in the console.
3981
+
3982
+ ```tsx
3983
+ Box.configure({ componentAttribute: true }); // in a module both the server and the browser run
3984
+ ```
3985
+
3986
+ An app's own `data-bk` in `props` wins. Set the flag before the first render, on both sides, or a hydrating browser sees attributes the server never wrote. The half of the channel that delivers and watches the page is a chunk of under 1.5 KB, fetched with the first subscription; in a test, `await vi.dynamicImportSettled()` after subscribing, before the first assertion.
3987
+
3988
+ **A moment of the app's own is `Box.reportInteraction`.** Box never wraps the handlers an app passes in `props`: wrapping them all would cost every render, report a hover or a scroll per frame, and name nothing better than `button`. So a hover, a section scrolled into view or a wizard step is reported where it happens, in the same shape, to the same listeners, fire and forget:
3989
+
3990
+ ```tsx
3991
+ <Button props={{ onMouseEnter: (event) => Box.reportInteraction({ component: 'pricing-cta', type: 'mouseEnter', event }) }}>Pricing</Button>
3992
+ ```
3993
+
3994
+ It is a plain function, not a hook, so it works in an effect, a fetch callback or a module with no React in it. `event` may be React's synthetic one; `target`, `source`, `timeStamp`, `time` and `url` are filled in. Keep it to discrete moments, the rule the components follow.
3995
+
3996
+ ### Sending it somewhere: `@box-kite/react/analytics`
3997
+
3998
+ `createAnalytics({ send })` is that stream collected: every interaction as a flat JSON record with a session id, batched, held until consent is granted, and sent as the page is hidden. No React and no engine, 1.9 KB gzipped.
3999
+
4000
+ ```ts
4001
+ import { beacon, consoleLog, createAnalytics } from '@box-kite/react/analytics';
4002
+
4003
+ const analytics = createAnalytics({
4004
+ send: [beacon('https://collect.example.com/events'), consoleLog()],
4005
+ consent: 'pending', // nothing stored or sent until the reader answers
4006
+ });
4007
+ analytics.consent('granted');
4008
+ ```
4009
+
4010
+ - **A record** is `{ id, name, kind, component, type, reason, value, details, source, trigger, label, time, url, sessionId, visitorId? }`: `name` is `component.type`, `trigger` the DOM event's type, `label` the words on a pressed button, link, tab or menu item (never a cell's or a field's without `content`), `time` ISO 8601 and `id` unique, so a collector sent a batch twice keeps it once.
4011
+ - **`sessionId` is one visit**, in `sessionStorage`: it survives a reload, ends with the tab, and starts again after `sessionTimeout` (30 minutes) with nothing happening. **`visitor: true`** adds a `visitorId` in `localStorage`, one browser across visits, written only once consent is granted.
4012
+ - **`consent`**: `'granted'` (default), `'pending'` (held in memory, nothing stored or sent) or `'denied'` (dropped, both ids forgotten); `analytics.consent(next)` answers it later.
4013
+ - **`content: true`** passes the stream's own option through. **`batch: { size, wait }`** (20, 5000 ms), **`filter(event)`** on the raw interaction, **`analytics.flush()`** and **`analytics.stop()`**.
4014
+ - **A transport is a function of the batch.** `beacon(url)` posts `{ "events": [...] }` with `sendBeacon` as `text/plain` (no preflight; read the body as JSON), falling back to a `keepalive` fetch. A vendor is one more: `send: (events) => events.forEach((e) => posthog.capture(e.name, e))`.
package/analytics.cjs ADDED
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./platform.cjs"),t=require("./interactions.cjs");var n=18e5;function r(){if(typeof crypto.randomUUID==`function`)return crypto.randomUUID();let e=Array.from(crypto.getRandomValues(new Uint8Array(16)),e=>e.toString(16).padStart(2,`0`)).join(``);return`${e.slice(0,8)}-${e.slice(8,12)}-4${e.slice(13,16)}-${`89ab`[parseInt(e[16],16)%4]}${e.slice(17,20)}-${e.slice(20,32)}`}function i(e,t){try{return e().getItem(t)}catch{return null}}function a(e,t,n){try{n===null?e().removeItem(t):e().setItem(t,n)}catch{}}var o=()=>sessionStorage,s=()=>localStorage;function c(e,t){let n=`${e}.session`,c=`${e}.visitor`,l,u;return{sessionAt(e){if(!l)try{l=JSON.parse(i(o,n)??`null`)??void 0}catch{l=void 0}return!l||e-l.last>t?l={id:r(),last:e}:l.last=Math.max(l.last,e),a(o,n,JSON.stringify(l)),l.id},visitor(){return u??=i(s,c)??r(),a(s,c,u),u},forget(){l=void 0,u=void 0,a(o,n,null),a(s,c,null)}}}var l=200,u=64,d=`button,a[href],[role=tab],[role=menuitem],[role=menuitemcheckbox],[role=menuitemradio],summary`;function f(e,t){if(!e||!t&&!e.matches(d))return;let n=e.getAttribute(`aria-label`)??e.textContent?.trim().replace(/\s+/g,` `);return n?n.length>u?`${n.slice(0,63)}…`:n:void 0}function p(i){let{send:a,visitor:o=!1,content:s=!1,sessionTimeout:u=n,filter:d,storageKey:p=`box-kite`}=i,m=i.batch?.size??20,h=i.batch?.wait??5e3,g=Array.isArray(a)?a:[a];if(!e.r())return{consent:()=>{},flush:()=>{},stop:()=>{}};let _=c(p,u),v=i.consent??`granted`,y=[],b,x=()=>{if(clearTimeout(b),b=void 0,v!==`granted`||y.length===0)return;let e=o?_.visitor():void 0,t=y.map(({at:t,...n})=>({...n,time:new Date(t).toISOString(),sessionId:_.sessionAt(t),...e&&{visitorId:e}}));y=[];for(let e=0;e<t.length;e+=m){let n=t.slice(e,e+m);g.forEach(e=>e(n))}},S=t.i(e=>{if(v===`denied`||d&&!d(e))return;let t={id:r(),name:`${e.component}.${e.type}`,kind:e.kind,component:e.component,type:e.type,reason:e.reason,value:e.value,details:e.details,source:e.source,trigger:e.event?.type,label:f(e.target,s),url:e.url,at:e.time};y.push(t),v===`pending`?y.length>l&&y.shift():y.length>=m?x():b??=setTimeout(x,h)},{content:s}),C=()=>{document.visibilityState===`hidden`&&(t.n(),x())},w=()=>{t.n(),x()};return document.addEventListener(`visibilitychange`,C),addEventListener(`pagehide`,w),{consent(e){v=e,e===`denied`?(y=[],_.forget()):x()},flush:w,stop(){w(),S(),document.removeEventListener(`visibilitychange`,C),removeEventListener(`pagehide`,w)}}}function m(e){return t=>{let n=JSON.stringify({events:t});navigator.sendBeacon?.(e,new Blob([n],{type:`text/plain;charset=UTF-8`}))||fetch(e,{method:`POST`,body:n,keepalive:!0,headers:{"content-type":`text/plain;charset=UTF-8`}}).catch(()=>{})}}function h(e=`[interaction]`){return t=>t.forEach(t=>console.info(e,t))}exports.beacon=m,exports.consoleLog=h,exports.createAnalytics=p;
package/analytics.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `Box.onInteraction`, collected (`@box-kite/react/analytics`): every interaction as a flat JSON record carrying a session id,
3
+ * batched, held until consent is granted and sent as the page is hidden. Framework-free: no React and no engine, only the channel.
4
+ *
5
+ * ```ts
6
+ * const analytics = createAnalytics({ send: beacon('/collect') });
7
+ * ```
8
+ */
9
+ export { beacon, consoleLog, createAnalytics } from './utils/analytics/analytics';
10
+ export type { Analytics, AnalyticsConsent, AnalyticsEvent, AnalyticsOptions, AnalyticsTransport } from './utils/analytics/analytics';
package/analytics.mjs ADDED
@@ -0,0 +1,121 @@
1
+ import { r as e } from "./platform.mjs";
2
+ import { i as t, n } from "./interactions.mjs";
3
+ //#region src/utils/analytics/analyticsIdentity.ts
4
+ var r = 18e5;
5
+ function i() {
6
+ if (typeof crypto.randomUUID == "function") return crypto.randomUUID();
7
+ let e = Array.from(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16)), (e) => e.toString(16).padStart(2, "0")).join("");
8
+ return `${e.slice(0, 8)}-${e.slice(8, 12)}-4${e.slice(13, 16)}-${"89ab"[parseInt(e[16], 16) % 4]}${e.slice(17, 20)}-${e.slice(20, 32)}`;
9
+ }
10
+ function a(e, t) {
11
+ try {
12
+ return e().getItem(t);
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+ function o(e, t, n) {
18
+ try {
19
+ n === null ? e().removeItem(t) : e().setItem(t, n);
20
+ } catch {}
21
+ }
22
+ var s = () => sessionStorage, c = () => localStorage;
23
+ function l(e, t) {
24
+ let n = `${e}.session`, r = `${e}.visitor`, l, u;
25
+ return {
26
+ sessionAt(e) {
27
+ if (!l) try {
28
+ l = JSON.parse(a(s, n) ?? "null") ?? void 0;
29
+ } catch {
30
+ l = void 0;
31
+ }
32
+ return !l || e - l.last > t ? l = {
33
+ id: i(),
34
+ last: e
35
+ } : l.last = Math.max(l.last, e), o(s, n, JSON.stringify(l)), l.id;
36
+ },
37
+ visitor() {
38
+ return u ??= a(c, r) ?? i(), o(c, r, u), u;
39
+ },
40
+ forget() {
41
+ l = void 0, u = void 0, o(s, n, null), o(c, r, null);
42
+ }
43
+ };
44
+ }
45
+ //#endregion
46
+ //#region src/utils/analytics/analytics.ts
47
+ var u = 200, d = 64, f = "button,a[href],[role=tab],[role=menuitem],[role=menuitemcheckbox],[role=menuitemradio],summary";
48
+ function p(e, t) {
49
+ if (!e || !t && !e.matches(f)) return;
50
+ let n = e.getAttribute("aria-label") ?? e.textContent?.trim().replace(/\s+/g, " ");
51
+ return n ? n.length > d ? `${n.slice(0, 63)}…` : n : void 0;
52
+ }
53
+ function m(a) {
54
+ let { send: o, visitor: s = !1, content: c = !1, sessionTimeout: d = r, filter: f, storageKey: m = "box-kite" } = a, h = a.batch?.size ?? 20, g = a.batch?.wait ?? 5e3, _ = Array.isArray(o) ? o : [o];
55
+ if (!e()) return {
56
+ consent: () => {},
57
+ flush: () => {},
58
+ stop: () => {}
59
+ };
60
+ let v = l(m, d), y = a.consent ?? "granted", b = [], x, S = () => {
61
+ if (clearTimeout(x), x = void 0, y !== "granted" || b.length === 0) return;
62
+ let e = s ? v.visitor() : void 0, t = b.map(({ at: t, ...n }) => ({
63
+ ...n,
64
+ time: new Date(t).toISOString(),
65
+ sessionId: v.sessionAt(t),
66
+ ...e && { visitorId: e }
67
+ }));
68
+ b = [];
69
+ for (let e = 0; e < t.length; e += h) {
70
+ let n = t.slice(e, e + h);
71
+ _.forEach((e) => e(n));
72
+ }
73
+ }, C = t((e) => {
74
+ if (y === "denied" || f && !f(e)) return;
75
+ let t = {
76
+ id: i(),
77
+ name: `${e.component}.${e.type}`,
78
+ kind: e.kind,
79
+ component: e.component,
80
+ type: e.type,
81
+ reason: e.reason,
82
+ value: e.value,
83
+ details: e.details,
84
+ source: e.source,
85
+ trigger: e.event?.type,
86
+ label: p(e.target, c),
87
+ url: e.url,
88
+ at: e.time
89
+ };
90
+ b.push(t), y === "pending" ? b.length > u && b.shift() : b.length >= h ? S() : x ??= setTimeout(S, g);
91
+ }, { content: c }), w = () => {
92
+ document.visibilityState === "hidden" && (n(), S());
93
+ }, T = () => {
94
+ n(), S();
95
+ };
96
+ return document.addEventListener("visibilitychange", w), addEventListener("pagehide", T), {
97
+ consent(e) {
98
+ y = e, e === "denied" ? (b = [], v.forget()) : S();
99
+ },
100
+ flush: T,
101
+ stop() {
102
+ T(), C(), document.removeEventListener("visibilitychange", w), removeEventListener("pagehide", T);
103
+ }
104
+ };
105
+ }
106
+ function h(e) {
107
+ return (t) => {
108
+ let n = JSON.stringify({ events: t });
109
+ navigator.sendBeacon?.(e, new Blob([n], { type: "text/plain;charset=UTF-8" })) || fetch(e, {
110
+ method: "POST",
111
+ body: n,
112
+ keepalive: !0,
113
+ headers: { "content-type": "text/plain;charset=UTF-8" }
114
+ }).catch(() => {});
115
+ };
116
+ }
117
+ function g(e = "[interaction]") {
118
+ return (t) => t.forEach((t) => console.info(e, t));
119
+ }
120
+ //#endregion
121
+ export { h as beacon, g as consoleLog, m as createAnalytics };
package/behavior.cjs CHANGED
@@ -1 +1 @@
1
- const e=require("./platform.cjs"),t=require("./effects.cjs"),n=require("./keyboard.cjs");let r=require("react");function i(e){let n=(0,r.useRef)(e);return t.n(()=>{n.current=e}),n}function a(e){let t=i(e);return(0,r.useCallback)((...e)=>t.current?.(...e),[t])}function o(e){let{value:t,defaultValue:n,onChange:o}=e,s=t!==void 0,[c,l]=(0,r.useState)(n),u=s?t:c,d=i(u),f=a(o);return[u,(0,r.useCallback)((e,t)=>{let n=typeof e==`function`?e(d.current):e;Object.is(n,d.current)||(l(n),f(n,t))},[f,d])]}var s=[];function c(e,t){let n=t.elements();return e.elements().some(e=>n.some(t=>e!==t&&e.contains(t)))}function l(){let e=s.filter(e=>!s.some(t=>t!==e&&c(e,t)));return e[e.length-1]}function u(n){let{enabled:r=!0,onDismiss:o,inside:c,escapeKey:u=!0,outsidePointer:d=!0}=n,f=i(c),p=a(o);t.n(()=>{if(!r)return;let t={elements:()=>(f.current??[]).map(t=>e.c(t)).filter(e=>e!==null)};s.push(t);let n=new AbortController,i=(e,t)=>document.addEventListener(e,t,{signal:n.signal});return u&&i(`keydown`,e=>{e.key!==`Escape`||e.defaultPrevented||l()===t&&p(`escape`,e)}),d&&i(`pointerdown`,t=>{e.u(t,f.current??[])||p(`outside-pointer`,t)}),()=>{n.abort();let e=s.indexOf(t);e!==-1&&s.splice(e,1)}},[r,u,d,p,f])}function d(){let t=e.t()?.activeElement;return t&&t!==document.body?t:null}function f(t={}){let{enabled:n=!0,returnTo:i,preventScroll:o}=t,[s,c]=(0,r.useState)(null),[l,u]=(0,r.useState)(!1);n!==l&&(u(n),n&&c(d()));let f=a(()=>{(e.l(i)??s)?.focus({preventScroll:o})});return(0,r.useEffect)(()=>{if(n)return()=>{let e=document.activeElement;(!e||e===document.body||!document.contains(e))&&f()}},[n,f]),{returnFocus:f}}var p=10;function m(e,t){return t<0?-1:Math.max(0,Math.min(e,t))}function h(e,t,n,r,i){if(n===0)return-1;let a=e;for(let e=0;e<n;e++){if(a+=t,a<0||a>=n){if(!r)return-1;a=(a%n+n)%n}if(!i?.(a))return a}return-1}function g(e,t,n){return h(e===1?-1:t,e,t,!1,n)}function _(t){let{count:a,columns:s,columnIndexOf:c,pageSize:l=p,defaultActiveColumn:u=0,onActiveCellChange:d,orientation:f=`vertical`,loop:_=!0,activeIndex:v,defaultActiveIndex:y=0,onActiveIndexChange:b,isDisabled:x,textOf:S,focusItems:C=!0,onSelect:w}=t,[T,E]=o({value:v,defaultValue:y,onChange:b}),D=Math.min(T,a-1),O=s!==void 0,k=(0,r.useCallback)(e=>typeof s==`function`?s(e):s??0,[s]),A=(0,r.useCallback)((e,t)=>{let n=k(e);if(!c)return m(t,n-1);if(n<=0)return-1;let r=0;for(;r+1<n&&c(e,r+1)<=t;)r++;return r},[c,k]),[j,M]=(0,r.useState)(u),N=O&&D>=0?A(D,j):-1,P=(0,r.useRef)([]),F=(0,r.useRef)(new Map),I=(0,r.useRef)(new Map),L=(0,r.useRef)(new Map),R=(0,r.useRef)({query:``,at:0}),z=(0,r.useCallback)(e=>{P.current[e]?.focus()},[]),B=(0,r.useRef)(!1),V=(0,r.useCallback)((e,t)=>{B.current=!0,I.current.get(`${e}:${t}`)?.focus(),B.current=!1},[]),H=(0,r.useCallback)(()=>O?I.current.get(`${D}:${N}`)??null:P.current[D]??null,[N,D,O]),U=i({row:D,column:N}),W=i(j),G=i(d),K=i(k),q=i(A),J=i(c),Y=(0,r.useCallback)((e,t,n)=>{let r=m(e,a-1),i=Math.max(0,t),o=r<0?-1:q.current(r,i),s=U.current;return r===s.row&&o===s.column?s:(E(r,n),M(i),G.current?.({row:r,column:o},n),{row:r,column:o})},[U,q,G,a,E]),X=(0,r.useCallback)((e,t,n)=>{let r=m(e,a-1),i=r<0?-1:m(Math.max(0,t),K.current(r)-1);return Y(r,i<0?0:J.current?.(r,i)??i,n)},[J,a,Y,K]),Z=(0,r.useCallback)((e,t)=>{let n=`${e}:${t}`;return L.current.has(n)||L.current.set(n,e=>{e?I.current.set(n,e):(I.current.delete(n),L.current.delete(n))}),{ref:L.current.get(n),tabIndex:e===D&&t===N?0:-1,onFocus:()=>{B.current||X(e,t,{reason:`focus`})}}},[N,D,X]),Q=(0,r.useCallback)(e=>(F.current.has(e)||F.current.set(e,t=>{P.current[e]=t}),{ref:F.current.get(e),tabIndex:C?e===D?0:-1:void 0,onFocus:C?()=>E(e,{reason:`focus`}):void 0}),[D,C,E]),$=(0,r.useCallback)(t=>{let{row:n,column:r}=U.current,i=(e,n)=>{t.preventDefault();let r=X(e,n,{reason:`keyboard`,event:t});C&&V(r.row,r.column)},o=e=>{t.preventDefault();let n=Y(e,W.current,{reason:`keyboard`,event:t});C&&V(n.row,n.column)},s=2**53-1,c=()=>e.d(t.currentTarget)?-1:1;switch(t.key){case`ArrowRight`:return i(n,r+c());case`ArrowLeft`:return i(n,r-c());case`ArrowDown`:return o(n+1);case`ArrowUp`:return o(n-1);case`Home`:return t.ctrlKey||t.metaKey?i(0,0):i(n,0);case`End`:return t.ctrlKey||t.metaKey?i(a-1,s):i(n,s);case`PageDown`:return o(n+l);case`PageUp`:return o(n-l)}},[U,a,W,V,C,l,Y,X]);return{activeIndex:D,activeColumn:N,setActiveIndex:E,setActiveCell:X,onKeyDown:(0,r.useCallback)(t=>{if(t.defaultPrevented)return;if(O){$(t);return}let r=f!==`horizontal`,i=f!==`vertical`,o=R.current,s=n.t.isOpen(o,t.timeStamp),c=(e,n)=>{e!==-1&&e!==D&&(t.preventDefault(),E(e,{reason:n,event:t}),C&&z(e))};if(i&&(t.key===`ArrowRight`||t.key===`ArrowLeft`)){let n=t.key===`ArrowRight`!==e.d(t.currentTarget);c(h(D,n?1:-1,a,_,x),`keyboard`);return}if(t.key===`ArrowDown`&&r){c(h(D,1,a,_,x),`keyboard`);return}if(t.key===`ArrowUp`&&r){c(h(D,-1,a,_,x),`keyboard`);return}if(t.key===`Home`){c(g(1,a,x),`keyboard`);return}if(t.key===`End`){c(g(-1,a,x),`keyboard`);return}if(t.key===`Enter`||t.key===` `&&!(s&&S)){if(!w||D===-1)return;t.preventDefault(),w(D,t);return}if(S&&n.t.isPrintable(t)){let e=n.t.push(o,t.key,t.timeStamp);c(n.t.target(e,D,a,S,x),`typeahead`)}},[D,a,z,C,x,O,_,$,w,f,E,S]),itemProps:Q,cellProps:Z,activeItem:H}}Object.defineProperty(exports,"a",{enumerable:!0,get:function(){return a}}),Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return o}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return f}}),Object.defineProperty(exports,"o",{enumerable:!0,get:function(){return i}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return u}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return _}});
1
+ const e=require("./platform.cjs"),t=require("./effects.cjs"),n=require("./keyboard.cjs");let r=require("react");function i(e){let n=(0,r.useRef)(e);return t.n(()=>{n.current=e}),n}function a(e){let t=i(e);return(0,r.useCallback)((...e)=>t.current?.(...e),[t])}function o(e){let{value:t,defaultValue:n,onChange:o}=e,s=t!==void 0,[c,l]=(0,r.useState)(n),u=s?t:c,d=i(u),f=a(o);return[u,(0,r.useCallback)((e,t)=>{let n=typeof e==`function`?e(d.current):e;Object.is(n,d.current)||(s||(d.current=n),l(n),f(n,t))},[f,d,s])]}var s=[];function c(e,t){let n=t.elements();return e.elements().some(e=>n.some(t=>e!==t&&e.contains(t)))}function l(){let e=s.filter(e=>!s.some(t=>t!==e&&c(e,t)));return e[e.length-1]}function u(n){let{enabled:r=!0,onDismiss:o,inside:c,escapeKey:u=!0,outsidePointer:d=!0}=n,f=i(c),p=a(o);t.n(()=>{if(!r)return;let t={elements:()=>(f.current??[]).map(t=>e.u(t)).filter(e=>e!==null)};s.push(t);let n=new AbortController,i=(e,t)=>document.addEventListener(e,t,{signal:n.signal});return u&&i(`keydown`,e=>{e.key!==`Escape`||e.defaultPrevented||l()===t&&p(`escape`,e)}),d&&i(`pointerdown`,t=>{e.f(t,f.current??[])||p(`outside-pointer`,t)}),()=>{n.abort();let e=s.indexOf(t);e!==-1&&s.splice(e,1)}},[r,u,d,p,f])}function d(){let t=e.t()?.activeElement;return t&&t!==document.body?t:null}function f(t={}){let{enabled:n=!0,returnTo:i,preventScroll:o}=t,[s,c]=(0,r.useState)(null),[l,u]=(0,r.useState)(!1);n!==l&&(u(n),n&&c(d()));let f=a(()=>{(e.d(i)??s)?.focus({preventScroll:o})});return(0,r.useEffect)(()=>{if(n)return()=>{let e=document.activeElement;(!e||e===document.body||!document.contains(e))&&f()}},[n,f]),{returnFocus:f}}var p=10;function m(e,t){return t<0?-1:Math.max(0,Math.min(e,t))}function h(e,t,n,r,i){if(n===0)return-1;let a=e;for(let e=0;e<n;e++){if(a+=t,a<0||a>=n){if(!r)return-1;a=(a%n+n)%n}if(!i?.(a))return a}return-1}function g(e,t,n){return h(e===1?-1:t,e,t,!1,n)}function _(t){let{count:a,columns:s,columnIndexOf:c,pageSize:l=p,defaultActiveColumn:u=0,onActiveCellChange:d,orientation:f=`vertical`,loop:_=!0,activeIndex:v,defaultActiveIndex:y=0,onActiveIndexChange:b,isDisabled:x,textOf:S,focusItems:C=!0,onSelect:w}=t,[T,E]=o({value:v,defaultValue:y,onChange:b}),D=Math.min(T,a-1),O=s!==void 0,k=(0,r.useCallback)(e=>typeof s==`function`?s(e):s??0,[s]),A=(0,r.useCallback)((e,t)=>{let n=k(e);if(!c)return m(t,n-1);if(n<=0)return-1;let r=0;for(;r+1<n&&c(e,r+1)<=t;)r++;return r},[c,k]),[j,M]=(0,r.useState)(u),N=O&&D>=0?A(D,j):-1,P=(0,r.useRef)([]),F=(0,r.useRef)(new Map),I=(0,r.useRef)(new Map),L=(0,r.useRef)(new Map),R=(0,r.useRef)({query:``,at:0}),z=(0,r.useCallback)(e=>{P.current[e]?.focus()},[]),B=(0,r.useRef)(!1),V=(0,r.useCallback)((e,t)=>{B.current=!0,I.current.get(`${e}:${t}`)?.focus(),B.current=!1},[]),H=(0,r.useCallback)(()=>O?I.current.get(`${D}:${N}`)??null:P.current[D]??null,[N,D,O]),U=i({row:D,column:N}),W=i(j),G=i(d),K=i(k),q=i(A),J=i(c),Y=(0,r.useCallback)((e,t,n)=>{let r=m(e,a-1),i=Math.max(0,t),o=r<0?-1:q.current(r,i),s=U.current;return r===s.row&&o===s.column?s:(E(r,n),M(i),G.current?.({row:r,column:o},n),{row:r,column:o})},[U,q,G,a,E]),X=(0,r.useCallback)((e,t,n)=>{let r=m(e,a-1),i=r<0?-1:m(Math.max(0,t),K.current(r)-1);return Y(r,i<0?0:J.current?.(r,i)??i,n)},[J,a,Y,K]),Z=(0,r.useCallback)((e,t)=>{let n=`${e}:${t}`;return L.current.has(n)||L.current.set(n,e=>{e?I.current.set(n,e):(I.current.delete(n),L.current.delete(n))}),{ref:L.current.get(n),tabIndex:e===D&&t===N?0:-1,onFocus:()=>{B.current||X(e,t,{reason:`focus`})}}},[N,D,X]),Q=(0,r.useCallback)(e=>(F.current.has(e)||F.current.set(e,t=>{P.current[e]=t}),{ref:F.current.get(e),tabIndex:C?e===D?0:-1:void 0,onFocus:C?()=>E(e,{reason:`focus`}):void 0}),[D,C,E]),$=(0,r.useCallback)(t=>{let{row:n,column:r}=U.current,i=(e,n)=>{t.preventDefault();let r=X(e,n,{reason:`keyboard`,event:t});C&&V(r.row,r.column)},o=e=>{t.preventDefault();let n=Y(e,W.current,{reason:`keyboard`,event:t});C&&V(n.row,n.column)},s=2**53-1,c=()=>e.p(t.currentTarget)?-1:1;switch(t.key){case`ArrowRight`:return i(n,r+c());case`ArrowLeft`:return i(n,r-c());case`ArrowDown`:return o(n+1);case`ArrowUp`:return o(n-1);case`Home`:return t.ctrlKey||t.metaKey?i(0,0):i(n,0);case`End`:return t.ctrlKey||t.metaKey?i(a-1,s):i(n,s);case`PageDown`:return o(n+l);case`PageUp`:return o(n-l)}},[U,a,W,V,C,l,Y,X]);return{activeIndex:D,activeColumn:N,setActiveIndex:E,setActiveCell:X,onKeyDown:(0,r.useCallback)(t=>{if(t.defaultPrevented)return;if(O){$(t);return}let r=f!==`horizontal`,i=f!==`vertical`,o=R.current,s=n.t.isOpen(o,t.timeStamp),c=(e,n)=>{e!==-1&&e!==D&&(t.preventDefault(),E(e,{reason:n,event:t}),C&&z(e))};if(i&&(t.key===`ArrowRight`||t.key===`ArrowLeft`)){let n=t.key===`ArrowRight`!==e.p(t.currentTarget);c(h(D,n?1:-1,a,_,x),`keyboard`);return}if(t.key===`ArrowDown`&&r){c(h(D,1,a,_,x),`keyboard`);return}if(t.key===`ArrowUp`&&r){c(h(D,-1,a,_,x),`keyboard`);return}if(t.key===`Home`){c(g(1,a,x),`keyboard`);return}if(t.key===`End`){c(g(-1,a,x),`keyboard`);return}if(t.key===`Enter`||t.key===` `&&!(s&&S)){if(!w||D===-1)return;t.preventDefault(),w(D,t);return}if(S&&n.t.isPrintable(t)){let e=n.t.push(o,t.key,t.timeStamp);c(n.t.target(e,D,a,S,x),`typeahead`)}},[D,a,z,C,x,O,_,$,w,f,E,S]),itemProps:Q,cellProps:Z,activeItem:H}}Object.defineProperty(exports,"a",{enumerable:!0,get:function(){return a}}),Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return o}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return f}}),Object.defineProperty(exports,"o",{enumerable:!0,get:function(){return i}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return u}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return _}});
package/behavior.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { c as e, d as t, l as n, t as r, u as i } from "./platform.mjs";
1
+ import { d as e, f as t, p as n, t as r, u as i } from "./platform.mjs";
2
2
  import { n as a } from "./effects.mjs";
3
3
  import { t as o } from "./keyboard.mjs";
4
4
  import { useCallback as s, useEffect as c, useRef as l, useState as u } from "react";
@@ -19,8 +19,12 @@ function p(e) {
19
19
  let { value: t, defaultValue: n, onChange: r } = e, i = t !== void 0, [a, o] = u(n), c = i ? t : a, l = d(c), p = f(r);
20
20
  return [c, s((e, t) => {
21
21
  let n = typeof e == "function" ? e(l.current) : e;
22
- Object.is(n, l.current) || (o(n), p(n, t));
23
- }, [p, l])];
22
+ Object.is(n, l.current) || (i || (l.current = n), o(n), p(n, t));
23
+ }, [
24
+ p,
25
+ l,
26
+ i
27
+ ])];
24
28
  }
25
29
  //#endregion
26
30
  //#region src/react/a11y/useDismiss.ts
@@ -33,21 +37,21 @@ function g() {
33
37
  let e = m.filter((e) => !m.some((t) => t !== e && h(e, t)));
34
38
  return e[e.length - 1];
35
39
  }
36
- function _(t) {
37
- let { enabled: n = !0, onDismiss: r, inside: o, escapeKey: s = !0, outsidePointer: c = !0 } = t, l = d(o), u = f(r);
40
+ function _(e) {
41
+ let { enabled: n = !0, onDismiss: r, inside: o, escapeKey: s = !0, outsidePointer: c = !0 } = e, l = d(o), u = f(r);
38
42
  a(() => {
39
43
  if (!n) return;
40
- let t = { elements: () => (l.current ?? []).map((t) => e(t)).filter((e) => e !== null) };
41
- m.push(t);
44
+ let e = { elements: () => (l.current ?? []).map((e) => i(e)).filter((e) => e !== null) };
45
+ m.push(e);
42
46
  let r = new AbortController(), a = (e, t) => document.addEventListener(e, t, { signal: r.signal });
43
- return s && a("keydown", (e) => {
44
- e.key !== "Escape" || e.defaultPrevented || g() === t && u("escape", e);
47
+ return s && a("keydown", (t) => {
48
+ t.key !== "Escape" || t.defaultPrevented || g() === e && u("escape", t);
45
49
  }), c && a("pointerdown", (e) => {
46
- i(e, l.current ?? []) || u("outside-pointer", e);
50
+ t(e, l.current ?? []) || u("outside-pointer", e);
47
51
  }), () => {
48
52
  r.abort();
49
- let e = m.indexOf(t);
50
- e !== -1 && m.splice(e, 1);
53
+ let t = m.indexOf(e);
54
+ t !== -1 && m.splice(t, 1);
51
55
  };
52
56
  }, [
53
57
  n,
@@ -63,18 +67,18 @@ function v() {
63
67
  let e = r()?.activeElement;
64
68
  return e && e !== document.body ? e : null;
65
69
  }
66
- function y(e = {}) {
67
- let { enabled: t = !0, returnTo: r, preventScroll: i } = e, [a, o] = u(null), [s, l] = u(!1);
68
- t !== s && (l(t), t && o(v()));
70
+ function y(t = {}) {
71
+ let { enabled: n = !0, returnTo: r, preventScroll: i } = t, [a, o] = u(null), [s, l] = u(!1);
72
+ n !== s && (l(n), n && o(v()));
69
73
  let d = f(() => {
70
- (n(r) ?? a)?.focus({ preventScroll: i });
74
+ (e(r) ?? a)?.focus({ preventScroll: i });
71
75
  });
72
76
  return c(() => {
73
- if (t) return () => {
77
+ if (n) return () => {
74
78
  let e = document.activeElement;
75
79
  (!e || e === document.body || !document.contains(e)) && d();
76
80
  };
77
- }, [t, d]), { returnFocus: d };
81
+ }, [n, d]), { returnFocus: d };
78
82
  }
79
83
  //#endregion
80
84
  //#region src/react/a11y/useRovingFocus.ts
@@ -98,11 +102,11 @@ function C(e, t, n) {
98
102
  return S(e === 1 ? -1 : t, e, t, !1, n);
99
103
  }
100
104
  function w(e) {
101
- let { count: n, columns: r, columnIndexOf: i, pageSize: a = b, defaultActiveColumn: c = 0, onActiveCellChange: f, orientation: m = "vertical", loop: h = !0, activeIndex: g, defaultActiveIndex: _ = 0, onActiveIndexChange: v, isDisabled: y, textOf: w, focusItems: T = !0, onSelect: E } = e, [D, O] = p({
105
+ let { count: t, columns: r, columnIndexOf: i, pageSize: a = b, defaultActiveColumn: c = 0, onActiveCellChange: f, orientation: m = "vertical", loop: h = !0, activeIndex: g, defaultActiveIndex: _ = 0, onActiveIndexChange: v, isDisabled: y, textOf: w, focusItems: T = !0, onSelect: E } = e, [D, O] = p({
102
106
  value: g,
103
107
  defaultValue: _,
104
108
  onChange: v
105
- }), k = Math.min(D, n - 1), A = r !== void 0, j = s((e) => typeof r == "function" ? r(e) : r ?? 0, [r]), M = s((e, t) => {
109
+ }), k = Math.min(D, t - 1), A = r !== void 0, j = s((e) => typeof r == "function" ? r(e) : r ?? 0, [r]), M = s((e, t) => {
106
110
  let n = j(e);
107
111
  if (!i) return x(t, n - 1);
108
112
  if (n <= 0) return -1;
@@ -123,8 +127,8 @@ function w(e) {
123
127
  ]), G = d({
124
128
  row: k,
125
129
  column: F
126
- }), K = d(N), q = d(f), J = d(j), Y = d(M), X = d(i), Z = s((e, t, r) => {
127
- let i = x(e, n - 1), a = Math.max(0, t), o = i < 0 ? -1 : Y.current(i, a), s = G.current;
130
+ }), K = d(N), q = d(f), J = d(j), Y = d(M), X = d(i), Z = s((e, n, r) => {
131
+ let i = x(e, t - 1), a = Math.max(0, n), o = i < 0 ? -1 : Y.current(i, a), s = G.current;
128
132
  return i === s.row && o === s.column ? s : (O(i, r), P(a), q.current?.({
129
133
  row: i,
130
134
  column: o
@@ -136,14 +140,14 @@ function w(e) {
136
140
  G,
137
141
  Y,
138
142
  q,
139
- n,
143
+ t,
140
144
  O
141
- ]), Q = s((e, t, r) => {
142
- let i = x(e, n - 1), a = i < 0 ? -1 : x(Math.max(0, t), J.current(i) - 1);
145
+ ]), Q = s((e, n, r) => {
146
+ let i = x(e, t - 1), a = i < 0 ? -1 : x(Math.max(0, n), J.current(i) - 1);
143
147
  return Z(i, a < 0 ? 0 : X.current?.(i, a) ?? a, r);
144
148
  }, [
145
149
  X,
146
- n,
150
+ t,
147
151
  Z,
148
152
  J
149
153
  ]), ee = s((e, t) => {
@@ -186,20 +190,20 @@ function w(e) {
186
190
  event: e
187
191
  });
188
192
  T && U(n.row, n.column);
189
- }, c = 2 ** 53 - 1, l = () => t(e.currentTarget) ? -1 : 1;
193
+ }, c = 2 ** 53 - 1, l = () => n(e.currentTarget) ? -1 : 1;
190
194
  switch (e.key) {
191
195
  case "ArrowRight": return o(r, i + l());
192
196
  case "ArrowLeft": return o(r, i - l());
193
197
  case "ArrowDown": return s(r + 1);
194
198
  case "ArrowUp": return s(r - 1);
195
199
  case "Home": return e.ctrlKey || e.metaKey ? o(0, 0) : o(r, 0);
196
- case "End": return e.ctrlKey || e.metaKey ? o(n - 1, c) : o(r, c);
200
+ case "End": return e.ctrlKey || e.metaKey ? o(t - 1, c) : o(r, c);
197
201
  case "PageDown": return s(r + a);
198
202
  case "PageUp": return s(r - a);
199
203
  }
200
204
  }, [
201
205
  G,
202
- n,
206
+ t,
203
207
  K,
204
208
  U,
205
209
  T,
@@ -225,24 +229,24 @@ function w(e) {
225
229
  }), T && V(t));
226
230
  };
227
231
  if (i && (e.key === "ArrowRight" || e.key === "ArrowLeft")) {
228
- let r = e.key === "ArrowRight" !== t(e.currentTarget);
229
- c(S(k, r ? 1 : -1, n, h, y), "keyboard");
232
+ let r = e.key === "ArrowRight" !== n(e.currentTarget);
233
+ c(S(k, r ? 1 : -1, t, h, y), "keyboard");
230
234
  return;
231
235
  }
232
236
  if (e.key === "ArrowDown" && r) {
233
- c(S(k, 1, n, h, y), "keyboard");
237
+ c(S(k, 1, t, h, y), "keyboard");
234
238
  return;
235
239
  }
236
240
  if (e.key === "ArrowUp" && r) {
237
- c(S(k, -1, n, h, y), "keyboard");
241
+ c(S(k, -1, t, h, y), "keyboard");
238
242
  return;
239
243
  }
240
244
  if (e.key === "Home") {
241
- c(C(1, n, y), "keyboard");
245
+ c(C(1, t, y), "keyboard");
242
246
  return;
243
247
  }
244
248
  if (e.key === "End") {
245
- c(C(-1, n, y), "keyboard");
249
+ c(C(-1, t, y), "keyboard");
246
250
  return;
247
251
  }
248
252
  if (e.key === "Enter" || e.key === " " && !(s && w)) {
@@ -251,12 +255,12 @@ function w(e) {
251
255
  return;
252
256
  }
253
257
  if (w && o.isPrintable(e)) {
254
- let t = o.push(a, e.key, e.timeStamp);
255
- c(o.target(t, k, n, w, y), "typeahead");
258
+ let n = o.push(a, e.key, e.timeStamp);
259
+ c(o.target(n, k, t, w, y), "typeahead");
256
260
  }
257
261
  }, [
258
262
  k,
259
- n,
263
+ t,
260
264
  V,
261
265
  T,
262
266
  y,
package/box.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});const e=require("./rolldown-runtime.cjs"),t=require("./react-shared.cjs"),n=require("./client.cjs");let r=require("react");r=e.t(r,1);let i=require("@box-kite/core");function a({tag:e,tagProps:t,render:n}){let[i,a]=(0,r.useState)(!1);return r.default.createElement(e,{...t,onMouseEnter:()=>a(!0),onMouseLeave:()=>a(!1)},n({isHover:i}))}function o(e,i){let{tag:o=`div`,children:s}=e,{classNames:c,styleElements:l}=n.i(e,o===`svg`),u=t.d(e,c);i&&(u.ref=i);let d=typeof s==`function`?r.default.createElement(a,{tag:o,tagProps:u,render:s}):r.default.createElement(o,u,s);return l?r.default.createElement(r.default.Fragment,null,l,d):d}var s=t.f(o,`Box`);s.extend=i.BoxExtends.extend,s.components=i.BoxExtends.components,s.palette=i.BoxExtends.palette,s.createComponents=i.BoxExtends.createComponents,s.Components=n.o,s.keyframes=i.BoxExtends.keyframes,s.spring=i.Springs.spring,s.Theme=n.n,s.useTheme=n.n.useTheme,s.useDesign=n.n.useDesign,s.getVariableValue=e=>(0,i.getDefaultEngine)().getVariableValue(e),s.configure=n.r.configure,s.viewTransition=i.startViewTransition;function c(e,r){return t.p(n.i(e,r?.svg===!0),e.className)}exports.default=s,exports.useBoxElement=o,exports.useClassNames=c,exports.useVisibility=n.a;
1
+ Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});const e=require("./rolldown-runtime.cjs"),t=require("./react-shared.cjs"),n=require("./client.cjs"),r=require("./interactions.cjs");let i=require("react");i=e.t(i,1);let a=require("@box-kite/core");function o({tag:e,tagProps:t,render:n}){let[r,a]=(0,i.useState)(!1);return i.default.createElement(e,{...t,onMouseEnter:()=>a(!0),onMouseLeave:()=>a(!1)},n({isHover:r}))}function s(e,r){let{tag:a=`div`,children:s}=e,{classNames:c,styleElements:l}=n.i(e,a===`svg`),u=t.d(e,c);r&&(u.ref=r);let d=typeof s==`function`?i.default.createElement(o,{tag:a,tagProps:u,render:s}):i.default.createElement(a,u,s);return l?i.default.createElement(i.default.Fragment,null,l,d):d}var c=t.f(s,`Box`);c.extend=a.BoxExtends.extend,c.components=a.BoxExtends.components,c.palette=a.BoxExtends.palette,c.createComponents=a.BoxExtends.createComponents,c.Components=n.o,c.keyframes=a.BoxExtends.keyframes,c.spring=a.Springs.spring,c.Theme=n.n,c.useTheme=n.n.useTheme,c.useDesign=n.n.useDesign,c.getVariableValue=e=>(0,a.getDefaultEngine)().getVariableValue(e),c.configure=n.r.configure,c.viewTransition=a.startViewTransition,c.onInteraction=r.i,c.reportInteraction=r.r,c.flushInteractions=r.n;function l(e,r){return t.p(n.i(e,r?.svg===!0),e.className)}exports.default=c,exports.useBoxElement=s,exports.useClassNames=l,exports.useVisibility=n.a;
package/box.d.ts CHANGED
@@ -8,6 +8,7 @@ import { ExtractElementFromTag } from './react/reactTypes';
8
8
  import { default as Theme } from './react/theme/theme';
9
9
  import { StylesContext } from './react/useStyles';
10
10
  import { ComponentsAndVariants } from '@box-kite/core/types';
11
+ import { flushInteractions, reportInteraction, subscribeInteractions } from './utils/interactions/interactionChannel';
11
12
  /**
12
13
  * Box's render as a hook, for a component that is a Box with defaults: called from its own `forwardRef`, it
13
14
  * makes that component one fiber rather than a wrapper around a second one. `Flex`, `H1` and `Button` are this.
@@ -39,6 +40,12 @@ interface BoxType {
39
40
  configure: typeof StylesContext.configure;
40
41
  /** Run a DOM change inside a view transition where the browser has one, and plainly where it has not. */
41
42
  viewTransition: typeof startViewTransition;
43
+ /** Hear every interaction (a component's change, a press on anything it styled, the app's own) until the returned function is called. `{ content: true }` adds what people typed. */
44
+ onInteraction: typeof subscribeInteractions;
45
+ /** Report a moment of the app's own — a page view, a hover, a wizard step — to the same listeners, fire and forget. */
46
+ reportInteraction: typeof reportInteraction;
47
+ /** Deliver what is queued now: what an analytics sender calls before its last batch, and a test before it asserts. */
48
+ flushInteractions: typeof flushInteractions;
42
49
  }
43
50
  declare const Box: BoxType;
44
51
  export default Box;
@@ -64,4 +71,6 @@ export declare function useClassNames<TKey extends keyof ComponentsAndVariants =
64
71
  }): BoxClassNames;
65
72
  export { useVisibility };
66
73
  export type { BoxClassNames, BoxClassNameProps };
74
+ export type { ComponentInteraction, ComponentInteractions, CustomInteraction, DomInteraction, InteractionEvent, InteractionJson, InteractionListener, InteractionOptions, InteractionReport, } from './utils/interactions/interactionChannel';
75
+ export type { BoxConfiguration } from './react/useStyles';
67
76
  export type { BoxComponentStyles, BoxStyleProps } from '@box-kite/core/types';