@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.
- package/BOX_AI_CONTEXT.md +76 -0
- package/BOX_KITE_AI_CONTEXT.md +76 -0
- package/analytics.cjs +1 -0
- package/analytics.d.ts +10 -0
- package/analytics.mjs +121 -0
- package/behavior.cjs +1 -1
- package/behavior.mjs +42 -38
- package/box.cjs +1 -1
- package/box.d.ts +9 -0
- package/box.mjs +17 -16
- package/catalog.cjs +1 -1
- package/catalog.mjs +8 -0
- package/client.cjs +1 -1
- package/client.mjs +120 -120
- package/components/accordion.cjs +1 -1
- package/components/accordion.mjs +19 -10
- package/components/agent.cjs +1 -1
- package/components/agent.mjs +21 -8
- package/components/calendar.cjs +1 -1
- package/components/calendar.d.ts +3 -1
- package/components/calendar.mjs +154 -142
- package/components/combobox.cjs +1 -1
- package/components/combobox.mjs +184 -174
- package/components/dashboard.cjs +1 -1
- package/components/dashboard.mjs +183 -170
- package/components/dataGrid/models/columnModel.d.ts +1 -0
- package/components/dataGrid/models/gridModel.d.ts +18 -0
- package/components/dataGrid/models/rangeModel.d.ts +3 -0
- package/components/dataGrid.cjs +3 -3
- package/components/dataGrid.mjs +987 -896
- package/components/dateField.cjs +1 -1
- package/components/dateField.mjs +160 -149
- package/components/datePicker.cjs +1 -1
- package/components/datePicker.mjs +226 -206
- package/components/dialog.cjs +1 -1
- package/components/dialog.mjs +38 -33
- package/components/dropdown.cjs +1 -1
- package/components/dropdown.mjs +107 -93
- package/components/form.cjs +1 -1
- package/components/form.mjs +22 -18
- package/components/menu.cjs +1 -1
- package/components/menu.d.ts +2 -0
- package/components/menu.mjs +196 -175
- package/components/overlay.cjs +1 -1
- package/components/overlay.mjs +7 -7
- package/components/popover.cjs +1 -1
- package/components/popover.mjs +4 -0
- package/components/radioGroup.cjs +1 -1
- package/components/radioGroup.mjs +10 -5
- package/components/slider.cjs +1 -1
- package/components/slider.mjs +84 -83
- package/components/sortableList.cjs +1 -1
- package/components/sortableList.mjs +158 -150
- package/components/swipeItem.cjs +1 -1
- package/components/swipeItem.mjs +92 -84
- package/components/tabs.cjs +1 -1
- package/components/tabs.mjs +10 -5
- package/components/timeZoneSelect.cjs +1 -1
- package/components/timeZoneSelect.mjs +40 -35
- package/components/toaster.cjs +1 -1
- package/components/toaster.mjs +151 -113
- package/dataGridDateParts.cjs +1 -1
- package/dataGridDateParts.mjs +56 -55
- package/dataGridList.cjs +1 -1
- package/dataGridList.mjs +430 -429
- package/effects.cjs +1 -1
- package/effects.mjs +1 -1
- package/interactionListening.cjs +1 -0
- package/interactionListening.mjs +117 -0
- package/interactions.cjs +1 -0
- package/interactions.mjs +25 -0
- package/layer.cjs +1 -1
- package/layer.mjs +68 -66
- package/package.json +132 -68
- package/placement.cjs +1 -1
- package/placement.mjs +10 -10
- package/platform.cjs +1 -1
- package/platform.mjs +44 -25
- package/react/interactions/interactions.d.ts +30 -0
- package/react/popover/usePopoverLayer.d.ts +3 -0
- package/react/useStyles.d.ts +8 -1
- package/react-shared.cjs +1 -1
- package/react-shared.mjs +49 -48
- package/reporting.cjs +1 -0
- package/reporting.mjs +51 -0
- package/scrolling.cjs +1 -1
- package/scrolling.mjs +1 -1
- package/supabase/functions/collect/index.d.ts +30 -0
- package/supabase/functions/collect/index.test.d.ts +1 -0
- package/utils/analytics/analytics.d.ts +72 -0
- package/utils/analytics/analyticsIdentity.d.ts +13 -0
- package/utils/interactions/componentAttribute.d.ts +6 -0
- package/utils/interactions/interactionChannel.d.ts +35 -0
- package/utils/interactions/interactionJson.d.ts +6 -0
- package/utils/interactions/interactionListening.d.ts +6 -0
- 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))`.
|
package/BOX_KITE_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))`.
|
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.
|
|
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 {
|
|
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
|
-
}, [
|
|
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 _(
|
|
37
|
-
let { enabled: n = !0, onDismiss: r, inside: o, escapeKey: s = !0, outsidePointer: c = !0 } =
|
|
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
|
|
41
|
-
m.push(
|
|
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", (
|
|
44
|
-
|
|
47
|
+
return s && a("keydown", (t) => {
|
|
48
|
+
t.key !== "Escape" || t.defaultPrevented || g() === e && u("escape", t);
|
|
45
49
|
}), c && a("pointerdown", (e) => {
|
|
46
|
-
|
|
50
|
+
t(e, l.current ?? []) || u("outside-pointer", e);
|
|
47
51
|
}), () => {
|
|
48
52
|
r.abort();
|
|
49
|
-
let
|
|
50
|
-
|
|
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(
|
|
67
|
-
let { enabled:
|
|
68
|
-
|
|
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
|
-
(
|
|
74
|
+
(e(r) ?? a)?.focus({ preventScroll: i });
|
|
71
75
|
});
|
|
72
76
|
return c(() => {
|
|
73
|
-
if (
|
|
77
|
+
if (n) return () => {
|
|
74
78
|
let e = document.activeElement;
|
|
75
79
|
(!e || e === document.body || !document.contains(e)) && d();
|
|
76
80
|
};
|
|
77
|
-
}, [
|
|
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:
|
|
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,
|
|
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,
|
|
127
|
-
let i = x(e,
|
|
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
|
-
|
|
143
|
+
t,
|
|
140
144
|
O
|
|
141
|
-
]), Q = s((e,
|
|
142
|
-
let i = x(e,
|
|
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
|
-
|
|
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 = () =>
|
|
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(
|
|
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
|
-
|
|
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" !==
|
|
229
|
-
c(S(k, r ? 1 : -1,
|
|
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,
|
|
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,
|
|
241
|
+
c(S(k, -1, t, h, y), "keyboard");
|
|
238
242
|
return;
|
|
239
243
|
}
|
|
240
244
|
if (e.key === "Home") {
|
|
241
|
-
c(C(1,
|
|
245
|
+
c(C(1, t, y), "keyboard");
|
|
242
246
|
return;
|
|
243
247
|
}
|
|
244
248
|
if (e.key === "End") {
|
|
245
|
-
c(C(-1,
|
|
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
|
|
255
|
-
c(o.target(
|
|
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
|
-
|
|
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
|
|
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';
|