react-zeugma 6.9.4 → 6.9.6
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/README.md +122 -68
- package/dist/devtools.cjs +2 -0
- package/dist/devtools.cjs.map +1 -0
- package/dist/devtools.d.cts +7 -0
- package/dist/devtools.d.ts +7 -0
- package/dist/devtools.js +2 -0
- package/dist/devtools.js.map +1 -0
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -27
- package/dist/index.d.ts +28 -27
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/render-counter-footer-CYe6-Atc.d.cts +116 -0
- package/dist/render-counter-footer-CYe6-Atc.d.ts +116 -0
- package/dist/{types-DFl4yzkP.d.cts → types-DQICRtw1.d.cts} +0 -9
- package/dist/{types-DFl4yzkP.d.ts → types-DQICRtw1.d.ts} +0 -9
- package/dist/utils.d.cts +1 -1
- package/dist/utils.d.ts +1 -1
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -96,29 +96,28 @@ import { Zeugma } from 'react-zeugma'
|
|
|
96
96
|
|
|
97
97
|
##### Props
|
|
98
98
|
|
|
99
|
-
| Property | Description
|
|
100
|
-
| ------------------------ |
|
|
101
|
-
| `controller` | The layout state controller returned by `useZeugma(options)`.
|
|
102
|
-
| `children` | Children components rendered inside the context provider.
|
|
103
|
-
| `renderPane` | Callback function to map active pane IDs to custom pane structures. Required in standalone mode (without children) and must not be passed in provider mode.
|
|
104
|
-
| `renderDragOverlay` | Custom overlay renderer function for the drag-under-cursor preview.
|
|
105
|
-
| `classNames` | CSS class name mapping overrides for custom dashboard and overlay styling.
|
|
106
|
-
| `resizerSize` | Thickness of the split resizer bars in pixels.
|
|
107
|
-
| `dragActivationDistance` | Minimum pointer drag distance (in pixels) required to activate dragging.
|
|
108
|
-
| `snapThreshold` | Threshold in pixels to snap layout resizers to adjacent edges.
|
|
109
|
-
| `minSplitPercentage` | Minimum split limit percentage allowed for resized panes.
|
|
110
|
-
| `maxSplitPercentage` | Maximum split limit percentage allowed for resized panes.
|
|
111
|
-
| `enableDragToDismiss` | Enables drag-out-to-dismiss gesture for widgets.
|
|
112
|
-
| `dismissThreshold` | Distance in pixels outside container bounds required to trigger dismissal.
|
|
113
|
-
| `onRemove` | Callback triggered when a pane is removed.
|
|
114
|
-
| `onDragStart` | Callback triggered when a drag gesture begins.
|
|
115
|
-
| `onDragEnd` | Callback triggered when a drag gesture ends, containing active pane, target pane, and action metadata.
|
|
116
|
-
| `onResizeStart` | Callback triggered when resizing begins.
|
|
117
|
-
| `onResize` | Callback triggered during pane resizing.
|
|
118
|
-
| `onResizeEnd` | Callback triggered when pane resizing completes.
|
|
119
|
-
| `onDismissIntentChange` | Callback triggered when drag-out dismiss intent changes.
|
|
120
|
-
| `persist` | Layout persistence configuration in localStorage. If true, uses default options.
|
|
121
|
-
| `renderPopoutWrapper` | **[Experimental]** Custom wrapper to inject context or style managers (e.g. `styled-components`' `StyleSheetManager` or `antd`'s `StyleProvider`/`ConfigProvider`) into popout windows. | `(props: { tabId: string; document: Document; window: Window; children: React.ReactNode }) => React.ReactNode` | - |
|
|
99
|
+
| Property | Description | Type | Default |
|
|
100
|
+
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
|
|
101
|
+
| `controller` | The layout state controller returned by `useZeugma(options)`. | `ZeugmaController` | - |
|
|
102
|
+
| `children` | Children components rendered inside the context provider. | `ReactNode` | - |
|
|
103
|
+
| `renderPane` | Callback function to map active pane IDs to custom pane structures. Required in standalone mode (without children) and must not be passed in provider mode. | `(paneId: string) => ReactNode` | - |
|
|
104
|
+
| `renderDragOverlay` | Custom overlay renderer function for the drag-under-cursor preview. | `(active: DragOverlayActiveItem) => ReactNode` | - |
|
|
105
|
+
| `classNames` | CSS class name mapping overrides for custom dashboard and overlay styling. | `ZeugmaClassNames` | - |
|
|
106
|
+
| `resizerSize` | Thickness of the split resizer bars in pixels. | `number` | `4` |
|
|
107
|
+
| `dragActivationDistance` | Minimum pointer drag distance (in pixels) required to activate dragging. | `number` | `8` |
|
|
108
|
+
| `snapThreshold` | Threshold in pixels to snap layout resizers to adjacent edges. | `number` | `8` |
|
|
109
|
+
| `minSplitPercentage` | Minimum split limit percentage allowed for resized panes. | `number` | `5` |
|
|
110
|
+
| `maxSplitPercentage` | Maximum split limit percentage allowed for resized panes. | `number` | `95` |
|
|
111
|
+
| `enableDragToDismiss` | Enables drag-out-to-dismiss gesture for widgets. | `boolean` | `false` |
|
|
112
|
+
| `dismissThreshold` | Distance in pixels outside container bounds required to trigger dismissal. | `number` | `60` |
|
|
113
|
+
| `onRemove` | Callback triggered when a pane is removed. | `(paneId: string) => void` | - |
|
|
114
|
+
| `onDragStart` | Callback triggered when a drag gesture begins. | `(activeId: string) => void` | - |
|
|
115
|
+
| `onDragEnd` | Callback triggered when a drag gesture ends, containing active pane, target pane, and action metadata. | `(activeId: string, overId: string \| null, dropAction: { type: 'split' \| 'move'; direction?: SplitDirection; position?: 'top' \| 'bottom' \| 'left' \| 'right' \| 'center' } \| null) => void` | - |
|
|
116
|
+
| `onResizeStart` | Callback triggered when resizing begins. | `(currentNode: SplitNode) => void` | - |
|
|
117
|
+
| `onResize` | Callback triggered during pane resizing. | `(currentNode: SplitNode, percentage: number) => void` | - |
|
|
118
|
+
| `onResizeEnd` | Callback triggered when pane resizing completes. | `(currentNode: SplitNode, percentage: number) => void` | - |
|
|
119
|
+
| `onDismissIntentChange` | Callback triggered when drag-out dismiss intent changes. | `(paneId: string \| null) => void` | - |
|
|
120
|
+
| `persist` | Layout persistence configuration in localStorage. If true, uses default options. | `boolean \| ZeugmaPersistOptions` | `false` |
|
|
122
121
|
|
|
123
122
|
##### `ZeugmaPersistOptions`
|
|
124
123
|
|
|
@@ -284,14 +283,14 @@ const controller = useZeugma({
|
|
|
284
283
|
|
|
285
284
|
##### Options
|
|
286
285
|
|
|
287
|
-
| Parameter | Description
|
|
288
|
-
| -------------------- |
|
|
289
|
-
| `initialLayout` | Initial layout tree structure. Only used on mount.
|
|
290
|
-
| `layout` | Controlled layout tree structure. Hook runs in controlled mode if provided.
|
|
291
|
-
| `onChange` | Callback triggered when the layout tree updates.
|
|
292
|
-
| `fullscreenPaneId` | Controlled fullscreen pane ID.
|
|
293
|
-
| `onFullscreenChange` | Callback triggered when fullscreen state toggles.
|
|
294
|
-
| `locked` | Global lock status to disable resizing and drag-and-drop operations.
|
|
286
|
+
| Parameter | Description | Type | Default |
|
|
287
|
+
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------- | ------- |
|
|
288
|
+
| `initialLayout` | Initial layout tree structure. Only used on mount. | `TreeNode \| null` | `null` |
|
|
289
|
+
| `layout` | Controlled layout tree structure. Hook runs in controlled mode if provided. | `TreeNode \| null` | `null` |
|
|
290
|
+
| `onChange` | Callback triggered when the layout tree updates. | `(newLayout: TreeNode \| null) => void` | - |
|
|
291
|
+
| `fullscreenPaneId` | Controlled fullscreen pane ID. When active (not null), structural layout changes (closes, additions, splits, merges, resizes, and popouts) are blocked at the API level. | `string \| null` | `null` |
|
|
292
|
+
| `onFullscreenChange` | Callback triggered when fullscreen state toggles. | `(paneId: string \| null) => void` | - |
|
|
293
|
+
| `locked` | Global lock status to disable resizing and drag-and-drop operations. | `boolean` | `false` |
|
|
295
294
|
|
|
296
295
|
---
|
|
297
296
|
|
|
@@ -310,32 +309,32 @@ const { layout, locked, setLocked, addTab, removePane, selectTab, findPaneById }
|
|
|
310
309
|
|
|
311
310
|
##### Context Values
|
|
312
311
|
|
|
313
|
-
| Property / Method | Description
|
|
314
|
-
| ----------------------- |
|
|
315
|
-
| `layout` | The current active layout tree structure.
|
|
316
|
-
| `fullscreenPaneId` | The ID of the maximized fullscreen pane.
|
|
317
|
-
| `locked` | Whether the dashboard layout is globally locked.
|
|
318
|
-
| `setLayout` | React state setter to update the layout tree.
|
|
319
|
-
| `setFullscreenPaneId` | Updates the active fullscreen pane ID.
|
|
320
|
-
| `setLocked` | Updates the global layout lock state.
|
|
321
|
-
| `removePane` | Removes a pane and collapses the split.
|
|
322
|
-
| `addTab` | Adds a tab to a pane, or splits/creates one if target is omitted.
|
|
323
|
-
| `updateMetadata` | Mutates a specific tab's metadata.
|
|
324
|
-
| `updatePaneLock` | Toggles the lock status of a specific pane.
|
|
325
|
-
| `selectTab` | Focuses/activates a tab within a pane.
|
|
326
|
-
| `mergeTab` | Programmatically drags and drops a tab from one pane to another.
|
|
327
|
-
| `removeTab` | Programmatically closes a tab.
|
|
328
|
-
| `splitPane` | Programmatically splits a pane node and adds a new one.
|
|
329
|
-
| `updateSplitPercentage` | Updates a SplitNode percentage.
|
|
330
|
-
| `moveTab` | Reorders a tab next to another, or swaps them if position is 'center'.
|
|
331
|
-
| `findPaneById` | Queries a PaneNode by its unique ID.
|
|
332
|
-
| `findPaneContainingTab` | Queries the parent PaneNode of a tab ID.
|
|
333
|
-
| `findTabById` | Queries detailed tab location and state metadata.
|
|
334
|
-
| `getTabMetadata` | Gets metadata for a tab ID.
|
|
335
|
-
| `getActiveTabMetadata` | Gets metadata for the active tab in a pane.
|
|
336
|
-
| `poppedOutTabIds` | **[Experimental]** The list of tab/widget IDs that are currently open in a new window.
|
|
337
|
-
| `popoutTab` | **[Experimental]** Popout the specified tab into a new window.
|
|
338
|
-
| `dockTab` | **[Experimental]** Dock the specified tab back to the main layout.
|
|
312
|
+
| Property / Method | Description | Type |
|
|
313
|
+
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
314
|
+
| `layout` | The current active layout tree structure. | `TreeNode \| null` |
|
|
315
|
+
| `fullscreenPaneId` | The ID of the maximized fullscreen pane. When active, structural layout changes (closes, additions, splits, merges, resizes, and popouts) are blocked at the API level. | `string \| null` |
|
|
316
|
+
| `locked` | Whether the dashboard layout is globally locked. | `boolean` |
|
|
317
|
+
| `setLayout` | React state setter to update the layout tree. | `Dispatch<SetStateAction<TreeNode \| null>>` |
|
|
318
|
+
| `setFullscreenPaneId` | Updates the active fullscreen pane ID. | `(paneId: string \| null) => void` |
|
|
319
|
+
| `setLocked` | Updates the global layout lock state. | `Dispatch<SetStateAction<boolean>>` |
|
|
320
|
+
| `removePane` | Removes a pane and collapses the split. | `(paneId: string) => void` |
|
|
321
|
+
| `addTab` | Adds a tab to a pane, or splits/creates one if target is omitted. | `(tabId: string, targetPaneId?: string, metadata?: Record<string, unknown>) => void` |
|
|
322
|
+
| `updateMetadata` | Mutates a specific tab's metadata. | `(id: string, updater: (current: Record<string, unknown> \| undefined) => Record<string, unknown> \| undefined) => void` |
|
|
323
|
+
| `updatePaneLock` | Toggles the lock status of a specific pane. | `(paneId: string, locked: boolean) => void` |
|
|
324
|
+
| `selectTab` | Focuses/activates a tab within a pane. | `(paneId: string, tabId: string) => void` |
|
|
325
|
+
| `mergeTab` | Programmatically drags and drops a tab from one pane to another. | `(draggedTabId: string, targetPaneId: string) => void` |
|
|
326
|
+
| `removeTab` | Programmatically closes a tab. | `(tabId: string) => void` |
|
|
327
|
+
| `splitPane` | Programmatically splits a pane node and adds a new one. | `(targetId: string, direction: SplitDirection, splitType: 'left' \| 'right' \| 'top' \| 'bottom', paneToAdd: string) => void` |
|
|
328
|
+
| `updateSplitPercentage` | Updates a SplitNode percentage. | `(currentNode: SplitNode, percentage: number) => void` |
|
|
329
|
+
| `moveTab` | Reorders a tab next to another, or swaps them if position is 'center'. | `(draggedTabId: string, targetTabId: string, position?: 'before' \| 'after' \| 'center') => void` |
|
|
330
|
+
| `findPaneById` | Queries a PaneNode by its unique ID. | `(paneId: string) => PaneNode \| null` |
|
|
331
|
+
| `findPaneContainingTab` | Queries the parent PaneNode of a tab ID. | `(tabId: string) => PaneNode \| null` |
|
|
332
|
+
| `findTabById` | Queries detailed tab location and state metadata. | `(tabId: string) => TabDetails \| null` |
|
|
333
|
+
| `getTabMetadata` | Gets metadata for a tab ID. | `(tabId: string) => Record<string, unknown> \| undefined` |
|
|
334
|
+
| `getActiveTabMetadata` | Gets metadata for the active tab in a pane. | `(paneId: string) => Record<string, unknown> \| undefined` |
|
|
335
|
+
| `poppedOutTabIds` | **[Experimental]** The list of tab/widget IDs that are currently open in a new window. | `string[]` |
|
|
336
|
+
| `popoutTab` | **[Experimental]** Popout the specified tab into a new window. | `(tabId: string) => void` |
|
|
337
|
+
| `dockTab` | **[Experimental]** Dock the specified tab back to the main layout. | `(tabId: string) => void` |
|
|
339
338
|
|
|
340
339
|
---
|
|
341
340
|
|
|
@@ -503,26 +502,81 @@ const parentPane = findPaneContainingTab(currentTree, 'new-file.js')
|
|
|
503
502
|
> [!WARNING]
|
|
504
503
|
> Popout window capabilities (opening tabs in separate browser windows via `window.open` and React portals) are **experimental** and may experience limitations or browser warnings depending on popup blockers, iframe restrictions, and context cleanup in third-party libraries. Use with caution in production.
|
|
505
504
|
|
|
506
|
-
|
|
505
|
+
> [!NOTE]
|
|
506
|
+
> React Zeugma automatically clones and synchronizes all static stylesheets, `<link>` tags, and document attributes (such as `data-theme` or body classes) from the main window to any active popout window in real-time. You only need `renderPopoutWrapper` to configure dynamic CSS-in-JS style injection and context provider redirection.
|
|
507
507
|
|
|
508
|
-
|
|
508
|
+
When using CSS-in-JS libraries like `styled-components` or `antd` (Ant Design) inside your widgets, dynamic styles are injected into the main document's `<head>` by default. In popped-out tabs, these dynamic styles will not apply because they render inside a separate window. Additionally, shared JS context caches will prevent styles from being re-emitted unless a separate cache instance is created.
|
|
509
|
+
|
|
510
|
+
To support this cleanly without causing widget remounts when popping windows out (which would reset the widget's internal state), you should wrap your widgets with the required style/theme providers **always** (both when docked and when popped out) directly inside the child render function of `<Pane.Content>`.
|
|
511
|
+
|
|
512
|
+
Because React Zeugma globally intercepts and patches `document` references (like `document.head` and `document.body`) during the portal rendering phase, you can pass the global `document` reference directly to the providers. It will automatically resolve to the correct active window's document.
|
|
509
513
|
|
|
510
514
|
##### Example (`styled-components` & `antd`):
|
|
511
515
|
|
|
512
516
|
```tsx
|
|
517
|
+
import React, { useMemo } from 'react'
|
|
513
518
|
import { StyleSheetManager } from 'styled-components'
|
|
514
|
-
import { StyleProvider } from '@ant-design/cssinjs'
|
|
519
|
+
import { StyleProvider, createCache } from '@ant-design/cssinjs'
|
|
515
520
|
import { ConfigProvider } from 'antd'
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
521
|
+
|
|
522
|
+
// 1. Create a wrapper component to safely instantiate and reuse a style cache
|
|
523
|
+
const StableStyleManager = ({ children }) => {
|
|
524
|
+
const cache = useMemo(() => createCache(), [])
|
|
525
|
+
|
|
526
|
+
return (
|
|
519
527
|
<StyleSheetManager target={document.head}>
|
|
520
|
-
<StyleProvider container={document.head}>
|
|
528
|
+
<StyleProvider cache={cache} container={document.head}>
|
|
521
529
|
<ConfigProvider getPopupContainer={() => document.body}>{children}</ConfigProvider>
|
|
522
530
|
</StyleProvider>
|
|
523
531
|
</StyleSheetManager>
|
|
524
|
-
)
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
532
|
+
)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// 2. Wrap your widget directly inside Pane.Content
|
|
536
|
+
const renderPane = (paneId: string) => (
|
|
537
|
+
<Pane id={paneId}>
|
|
538
|
+
<Pane.Content>
|
|
539
|
+
{(tab) => (
|
|
540
|
+
<StableStyleManager>
|
|
541
|
+
<Widget tabId={tab.id} />
|
|
542
|
+
</StableStyleManager>
|
|
543
|
+
)}
|
|
544
|
+
</Pane.Content>
|
|
545
|
+
</Pane>
|
|
546
|
+
)
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## DevTools & Performance Profiling (`react-zeugma/devtools`)
|
|
552
|
+
|
|
553
|
+
`react-zeugma` includes built-in DevTools utilities to audit component mount lifecycles and render frequencies across complex dashboard layouts.
|
|
554
|
+
|
|
555
|
+
### Usage
|
|
556
|
+
|
|
557
|
+
```tsx
|
|
558
|
+
import { useRenderCounter, RenderCounterBadge, RenderCounterFooter } from 'react-zeugma/devtools'
|
|
559
|
+
// Or directly from 'react-zeugma'
|
|
560
|
+
// import { useRenderCounter, RenderCounterBadge, RenderCounterFooter } from 'react-zeugma'
|
|
561
|
+
|
|
562
|
+
function MyDashboardWidget({ id }: { id: string }) {
|
|
563
|
+
// Hook usage
|
|
564
|
+
const { mounts, renders, reset } = useRenderCounter(id, { logToConsole: true })
|
|
565
|
+
|
|
566
|
+
return (
|
|
567
|
+
<RenderCounterFooter label={id} id={id}>
|
|
568
|
+
<div className="p-4">
|
|
569
|
+
<p>Mount count: {mounts}</p>
|
|
570
|
+
<p>Render count: {renders}</p>
|
|
571
|
+
<button onClick={reset}>Reset Counters</button>
|
|
572
|
+
</div>
|
|
573
|
+
</RenderCounterFooter>
|
|
574
|
+
)
|
|
575
|
+
}
|
|
528
576
|
```
|
|
577
|
+
|
|
578
|
+
### Components & Hooks
|
|
579
|
+
|
|
580
|
+
- `useRenderCounter(idOrOptions?, options?)`: React 18/19 StrictMode-safe hook returning `{ mounts, renders, reset }`.
|
|
581
|
+
- `<RenderCounterBadge id={id} position="top-right" />`: Floating overlay badge for any panel or component.
|
|
582
|
+
- `<RenderCounterFooter id={id} label="Widget Name">`: Wrapper component rendering a bottom status footer for tabbed widgets.
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
'use strict';var react=require('react'),jsxRuntime=require('react/jsx-runtime');var l=new Map;function a(t){let e=l.get(t);return e||(e={mounts:0,renders:0,listeners:new Set},l.set(t,e)),e}function N(t,e){let o=a(t);return o.listeners.add(e),()=>{o.listeners.delete(e);}}function R(t){let e=l.get(t);e&&e.listeners.forEach(o=>o());}function x(t){let e=l.get(t);e&&(e.mounts=0,e.renders=0,R(t));}function z(){l.clear();}function p(t,e){let o=react.useId(),c=typeof t=="string"?{id:t,...e}:t||{},{id:r=o,logToConsole:u=false,disabled:s=false}=c,n=react.useRef(0),d=react.useRef(false),[,C]=react.useState(0);if(s||(n.current+=1),react.useEffect(()=>{if(s)return;let g=N(r,()=>{C(S=>S+1);}),v=a(r);return v.mounts+=1,d.current=true,u&&console.log(`[DevTools:${r}] Mounted: ${v.mounts} | Rendered: ${n.current}`),R(r),()=>{d.current=false,g();}},[r,s,u]),!s){let g=a(r);g.renders=Math.max(g.renders,n.current);}let m=react.useCallback(()=>{n.current=0,x(r);},[r]);return s?{mounts:0,renders:0,reset:m}:{mounts:a(r).mounts,renders:n.current,reset:m}}var w={"top-right":"top-2 right-2","top-left":"top-2 left-2","bottom-right":"bottom-2 right-2","bottom-left":"bottom-2 left-2"};function F({id:t,position:e="top-right",className:o="",style:c,logToConsole:r=false,disabled:u=false}){let{mounts:s,renders:n}=p(t,{logToConsole:r,disabled:u});if(u)return null;let d=w[e]||w["top-right"];return jsxRuntime.jsxs("div",{className:`absolute ${d} z-50 pointer-events-none flex items-center gap-2 rounded px-2 py-1 text-[11px] font-mono bg-zinc-900/90 text-zinc-200 border border-zinc-700/80 shadow-md backdrop-blur-xs select-none ${o}`,style:c,children:[jsxRuntime.jsxs("span",{children:["M: ",jsxRuntime.jsx("strong",{className:"text-emerald-400 font-semibold",children:s})]}),jsxRuntime.jsx("span",{className:"opacity-30",children:"|"}),jsxRuntime.jsxs("span",{children:["R: ",jsxRuntime.jsx("strong",{className:"text-indigo-400 font-semibold",children:n})]})]})}function I({id:t,label:e,children:o,className:c="",footerClassName:r="",style:u,logToConsole:s=false,disabled:n=false}){let d=e||t||"widget",{mounts:C,renders:m}=p(t,{logToConsole:s,disabled:n});return n?jsxRuntime.jsx(jsxRuntime.Fragment,{children:o}):jsxRuntime.jsxs("div",{className:`h-full w-full flex flex-col overflow-hidden min-h-0 ${c}`,style:u,children:[o&&jsxRuntime.jsx("div",{className:"flex-1 overflow-hidden relative min-h-0",children:o}),jsxRuntime.jsxs("div",{className:`bg-zinc-900/90 border-t border-zinc-800 px-3 py-1.5 flex items-center justify-between text-[11px] text-zinc-400 font-mono shrink-0 select-none ${r}`,children:[jsxRuntime.jsx("span",{className:"truncate max-w-[140px] text-zinc-300 font-semibold",children:d}),jsxRuntime.jsxs("div",{className:"flex items-center gap-3",children:[jsxRuntime.jsxs("span",{children:["Mounts: ",jsxRuntime.jsx("strong",{className:"text-emerald-400 font-semibold",children:C})]}),jsxRuntime.jsx("span",{className:"opacity-20",children:"|"}),jsxRuntime.jsxs("span",{children:["Renders: ",jsxRuntime.jsx("strong",{className:"text-indigo-400 font-semibold",children:m})]})]})]})]})}exports.RenderCounterBadge=F;exports.RenderCounterFooter=I;exports.clearAllCounters=z;exports.resetCounterRecord=x;exports.useRenderCounter=p;//# sourceMappingURL=devtools.cjs.map
|
|
2
|
+
//# sourceMappingURL=devtools.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/devtools/store.ts","../src/devtools/use-render-counter.ts","../src/devtools/render-counter-badge.tsx","../src/devtools/render-counter-footer.tsx"],"names":["store","getCounterRecord","id","record","subscribeCounter","listener","notifyCounter","fn","resetCounterRecord","clearAllCounters","useRenderCounter","idOrOptions","maybeOptions","defaultAutoId","useId","options","logToConsole","disabled","renderCountRef","useRef","isMountedRef","forceUpdate","useState","useEffect","unsubscribe","c","reset","useCallback","POSITION_STYLES","RenderCounterBadge","position","className","style","mounts","renders","posClass","jsxs","jsx","RenderCounterFooter","label","children","footerClassName","displayId","Fragment"],"mappings":"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","file":"devtools.cjs","sourcesContent":["type Listener = () => void\n\ninterface GlobalCounterRecord {\n mounts: number\n renders: number\n listeners: Set<Listener>\n}\n\nconst store = new Map<string, GlobalCounterRecord>()\n\nexport function getCounterRecord(id: string): GlobalCounterRecord {\n let record = store.get(id)\n if (!record) {\n record = { mounts: 0, renders: 0, listeners: new Set() }\n store.set(id, record)\n }\n return record\n}\n\nexport function subscribeCounter(id: string, listener: Listener): () => void {\n const record = getCounterRecord(id)\n record.listeners.add(listener)\n return () => {\n record.listeners.delete(listener)\n }\n}\n\nexport function notifyCounter(id: string) {\n const record = store.get(id)\n if (record) {\n record.listeners.forEach((fn) => fn())\n }\n}\n\nexport function resetCounterRecord(id: string) {\n const record = store.get(id)\n if (record) {\n record.mounts = 0\n record.renders = 0\n notifyCounter(id)\n }\n}\n\nexport function clearAllCounters() {\n store.clear()\n}\n","'use client'\n\nimport { useRef, useEffect, useState, useCallback, useId } from 'react'\nimport type { RenderCounterOptions, RenderCounterState } from './types'\nimport { getCounterRecord, subscribeCounter, notifyCounter, resetCounterRecord } from './store'\n\n/**\n * A React hook that tracks component mount and render counts in a React 18/19 StrictMode safe manner.\n * Supports optional global ID sharing, console logging, and manual reset.\n */\nexport function useRenderCounter(\n idOrOptions?: string | RenderCounterOptions,\n maybeOptions?: RenderCounterOptions,\n): RenderCounterState {\n const defaultAutoId = useId()\n\n const options: RenderCounterOptions =\n typeof idOrOptions === 'string' ? { id: idOrOptions, ...maybeOptions } : idOrOptions || {}\n\n const { id = defaultAutoId, logToConsole = false, disabled = false } = options\n\n const renderCountRef = useRef<number>(0)\n const isMountedRef = useRef<boolean>(false)\n const [, forceUpdate] = useState<number>(0)\n\n if (!disabled) {\n renderCountRef.current += 1\n }\n\n useEffect(() => {\n if (disabled) return\n\n const unsubscribe = subscribeCounter(id, () => {\n forceUpdate((c) => c + 1)\n })\n\n const record = getCounterRecord(id)\n record.mounts += 1\n isMountedRef.current = true\n\n if (logToConsole) {\n console.log(\n `[DevTools:${id}] Mounted: ${record.mounts} | Rendered: ${renderCountRef.current}`,\n )\n }\n\n notifyCounter(id)\n\n return () => {\n isMountedRef.current = false\n unsubscribe()\n }\n }, [id, disabled, logToConsole])\n\n if (!disabled) {\n const record = getCounterRecord(id)\n record.renders = Math.max(record.renders, renderCountRef.current)\n }\n\n const reset = useCallback(() => {\n renderCountRef.current = 0\n resetCounterRecord(id)\n }, [id])\n\n if (disabled) {\n return { mounts: 0, renders: 0, reset }\n }\n\n const record = getCounterRecord(id)\n\n return {\n mounts: record.mounts,\n renders: renderCountRef.current,\n reset,\n }\n}\n","'use client'\n\nimport type { RenderCounterBadgeProps } from './types'\nimport { useRenderCounter } from './use-render-counter'\n\nconst POSITION_STYLES: Record<NonNullable<RenderCounterBadgeProps['position']>, string> = {\n 'top-right': 'top-2 right-2',\n 'top-left': 'top-2 left-2',\n 'bottom-right': 'bottom-2 right-2',\n 'bottom-left': 'bottom-2 left-2',\n}\n\n/**\n * A floating overlay badge displaying mount and render counters for debugging component lifecycles.\n */\nexport function RenderCounterBadge({\n id,\n position = 'top-right',\n className = '',\n style,\n logToConsole = false,\n disabled = false,\n}: RenderCounterBadgeProps) {\n const { mounts, renders } = useRenderCounter(id, { logToConsole, disabled })\n\n if (disabled) {\n return null\n }\n\n const posClass = POSITION_STYLES[position] || POSITION_STYLES['top-right']\n\n return (\n <div\n className={`absolute ${posClass} z-50 pointer-events-none flex items-center gap-2 rounded px-2 py-1 text-[11px] font-mono bg-zinc-900/90 text-zinc-200 border border-zinc-700/80 shadow-md backdrop-blur-xs select-none ${className}`}\n style={style}\n >\n <span>\n M: <strong className=\"text-emerald-400 font-semibold\">{mounts}</strong>\n </span>\n <span className=\"opacity-30\">|</span>\n <span>\n R: <strong className=\"text-indigo-400 font-semibold\">{renders}</strong>\n </span>\n </div>\n )\n}\n","'use client'\n\nimport type { RenderCounterFooterProps } from './types'\nimport { useRenderCounter } from './use-render-counter'\n\n/**\n * A container footer component for displaying mount and render counters at the bottom of widgets or panels.\n */\nexport function RenderCounterFooter({\n id,\n label,\n children,\n className = '',\n footerClassName = '',\n style,\n logToConsole = false,\n disabled = false,\n}: RenderCounterFooterProps) {\n const displayId = label || id || 'widget'\n const { mounts, renders } = useRenderCounter(id, { logToConsole, disabled })\n\n if (disabled) {\n return <>{children}</>\n }\n\n return (\n <div\n className={`h-full w-full flex flex-col overflow-hidden min-h-0 ${className}`}\n style={style}\n >\n {children && <div className=\"flex-1 overflow-hidden relative min-h-0\">{children}</div>}\n <div\n className={`bg-zinc-900/90 border-t border-zinc-800 px-3 py-1.5 flex items-center justify-between text-[11px] text-zinc-400 font-mono shrink-0 select-none ${footerClassName}`}\n >\n <span className=\"truncate max-w-[140px] text-zinc-300 font-semibold\">{displayId}</span>\n <div className=\"flex items-center gap-3\">\n <span>\n Mounts: <strong className=\"text-emerald-400 font-semibold\">{mounts}</strong>\n </span>\n <span className=\"opacity-20\">|</span>\n <span>\n Renders: <strong className=\"text-indigo-400 font-semibold\">{renders}</strong>\n </span>\n </div>\n </div>\n </div>\n )\n}\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { R as RenderCounterBadge, a as RenderCounterBadgeProps, b as RenderCounterFooter, c as RenderCounterFooterProps, d as RenderCounterOptions, e as RenderCounterState, u as useRenderCounter } from './render-counter-footer-CYe6-Atc.cjs';
|
|
2
|
+
import 'react';
|
|
3
|
+
|
|
4
|
+
declare function resetCounterRecord(id: string): void;
|
|
5
|
+
declare function clearAllCounters(): void;
|
|
6
|
+
|
|
7
|
+
export { clearAllCounters, resetCounterRecord };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { R as RenderCounterBadge, a as RenderCounterBadgeProps, b as RenderCounterFooter, c as RenderCounterFooterProps, d as RenderCounterOptions, e as RenderCounterState, u as useRenderCounter } from './render-counter-footer-CYe6-Atc.js';
|
|
2
|
+
import 'react';
|
|
3
|
+
|
|
4
|
+
declare function resetCounterRecord(id: string): void;
|
|
5
|
+
declare function clearAllCounters(): void;
|
|
6
|
+
|
|
7
|
+
export { clearAllCounters, resetCounterRecord };
|
package/dist/devtools.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import {useId,useRef,useState,useEffect,useCallback}from'react';import {jsxs,jsx,Fragment}from'react/jsx-runtime';var l=new Map;function a(t){let e=l.get(t);return e||(e={mounts:0,renders:0,listeners:new Set},l.set(t,e)),e}function N(t,e){let o=a(t);return o.listeners.add(e),()=>{o.listeners.delete(e);}}function R(t){let e=l.get(t);e&&e.listeners.forEach(o=>o());}function x(t){let e=l.get(t);e&&(e.mounts=0,e.renders=0,R(t));}function z(){l.clear();}function p(t,e){let o=useId(),c=typeof t=="string"?{id:t,...e}:t||{},{id:r=o,logToConsole:u=false,disabled:s=false}=c,n=useRef(0),d=useRef(false),[,C]=useState(0);if(s||(n.current+=1),useEffect(()=>{if(s)return;let g=N(r,()=>{C(S=>S+1);}),v=a(r);return v.mounts+=1,d.current=true,u&&console.log(`[DevTools:${r}] Mounted: ${v.mounts} | Rendered: ${n.current}`),R(r),()=>{d.current=false,g();}},[r,s,u]),!s){let g=a(r);g.renders=Math.max(g.renders,n.current);}let m=useCallback(()=>{n.current=0,x(r);},[r]);return s?{mounts:0,renders:0,reset:m}:{mounts:a(r).mounts,renders:n.current,reset:m}}var w={"top-right":"top-2 right-2","top-left":"top-2 left-2","bottom-right":"bottom-2 right-2","bottom-left":"bottom-2 left-2"};function F({id:t,position:e="top-right",className:o="",style:c,logToConsole:r=false,disabled:u=false}){let{mounts:s,renders:n}=p(t,{logToConsole:r,disabled:u});if(u)return null;let d=w[e]||w["top-right"];return jsxs("div",{className:`absolute ${d} z-50 pointer-events-none flex items-center gap-2 rounded px-2 py-1 text-[11px] font-mono bg-zinc-900/90 text-zinc-200 border border-zinc-700/80 shadow-md backdrop-blur-xs select-none ${o}`,style:c,children:[jsxs("span",{children:["M: ",jsx("strong",{className:"text-emerald-400 font-semibold",children:s})]}),jsx("span",{className:"opacity-30",children:"|"}),jsxs("span",{children:["R: ",jsx("strong",{className:"text-indigo-400 font-semibold",children:n})]})]})}function I({id:t,label:e,children:o,className:c="",footerClassName:r="",style:u,logToConsole:s=false,disabled:n=false}){let d=e||t||"widget",{mounts:C,renders:m}=p(t,{logToConsole:s,disabled:n});return n?jsx(Fragment,{children:o}):jsxs("div",{className:`h-full w-full flex flex-col overflow-hidden min-h-0 ${c}`,style:u,children:[o&&jsx("div",{className:"flex-1 overflow-hidden relative min-h-0",children:o}),jsxs("div",{className:`bg-zinc-900/90 border-t border-zinc-800 px-3 py-1.5 flex items-center justify-between text-[11px] text-zinc-400 font-mono shrink-0 select-none ${r}`,children:[jsx("span",{className:"truncate max-w-[140px] text-zinc-300 font-semibold",children:d}),jsxs("div",{className:"flex items-center gap-3",children:[jsxs("span",{children:["Mounts: ",jsx("strong",{className:"text-emerald-400 font-semibold",children:C})]}),jsx("span",{className:"opacity-20",children:"|"}),jsxs("span",{children:["Renders: ",jsx("strong",{className:"text-indigo-400 font-semibold",children:m})]})]})]})]})}export{F as RenderCounterBadge,I as RenderCounterFooter,z as clearAllCounters,x as resetCounterRecord,p as useRenderCounter};//# sourceMappingURL=devtools.js.map
|
|
2
|
+
//# sourceMappingURL=devtools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/devtools/store.ts","../src/devtools/use-render-counter.ts","../src/devtools/render-counter-badge.tsx","../src/devtools/render-counter-footer.tsx"],"names":["store","getCounterRecord","id","record","subscribeCounter","listener","notifyCounter","fn","resetCounterRecord","clearAllCounters","useRenderCounter","idOrOptions","maybeOptions","defaultAutoId","useId","options","logToConsole","disabled","renderCountRef","useRef","isMountedRef","forceUpdate","useState","useEffect","unsubscribe","c","reset","useCallback","POSITION_STYLES","RenderCounterBadge","position","className","style","mounts","renders","posClass","jsxs","jsx","RenderCounterFooter","label","children","footerClassName","displayId","Fragment"],"mappings":"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","file":"devtools.js","sourcesContent":["type Listener = () => void\n\ninterface GlobalCounterRecord {\n mounts: number\n renders: number\n listeners: Set<Listener>\n}\n\nconst store = new Map<string, GlobalCounterRecord>()\n\nexport function getCounterRecord(id: string): GlobalCounterRecord {\n let record = store.get(id)\n if (!record) {\n record = { mounts: 0, renders: 0, listeners: new Set() }\n store.set(id, record)\n }\n return record\n}\n\nexport function subscribeCounter(id: string, listener: Listener): () => void {\n const record = getCounterRecord(id)\n record.listeners.add(listener)\n return () => {\n record.listeners.delete(listener)\n }\n}\n\nexport function notifyCounter(id: string) {\n const record = store.get(id)\n if (record) {\n record.listeners.forEach((fn) => fn())\n }\n}\n\nexport function resetCounterRecord(id: string) {\n const record = store.get(id)\n if (record) {\n record.mounts = 0\n record.renders = 0\n notifyCounter(id)\n }\n}\n\nexport function clearAllCounters() {\n store.clear()\n}\n","'use client'\n\nimport { useRef, useEffect, useState, useCallback, useId } from 'react'\nimport type { RenderCounterOptions, RenderCounterState } from './types'\nimport { getCounterRecord, subscribeCounter, notifyCounter, resetCounterRecord } from './store'\n\n/**\n * A React hook that tracks component mount and render counts in a React 18/19 StrictMode safe manner.\n * Supports optional global ID sharing, console logging, and manual reset.\n */\nexport function useRenderCounter(\n idOrOptions?: string | RenderCounterOptions,\n maybeOptions?: RenderCounterOptions,\n): RenderCounterState {\n const defaultAutoId = useId()\n\n const options: RenderCounterOptions =\n typeof idOrOptions === 'string' ? { id: idOrOptions, ...maybeOptions } : idOrOptions || {}\n\n const { id = defaultAutoId, logToConsole = false, disabled = false } = options\n\n const renderCountRef = useRef<number>(0)\n const isMountedRef = useRef<boolean>(false)\n const [, forceUpdate] = useState<number>(0)\n\n if (!disabled) {\n renderCountRef.current += 1\n }\n\n useEffect(() => {\n if (disabled) return\n\n const unsubscribe = subscribeCounter(id, () => {\n forceUpdate((c) => c + 1)\n })\n\n const record = getCounterRecord(id)\n record.mounts += 1\n isMountedRef.current = true\n\n if (logToConsole) {\n console.log(\n `[DevTools:${id}] Mounted: ${record.mounts} | Rendered: ${renderCountRef.current}`,\n )\n }\n\n notifyCounter(id)\n\n return () => {\n isMountedRef.current = false\n unsubscribe()\n }\n }, [id, disabled, logToConsole])\n\n if (!disabled) {\n const record = getCounterRecord(id)\n record.renders = Math.max(record.renders, renderCountRef.current)\n }\n\n const reset = useCallback(() => {\n renderCountRef.current = 0\n resetCounterRecord(id)\n }, [id])\n\n if (disabled) {\n return { mounts: 0, renders: 0, reset }\n }\n\n const record = getCounterRecord(id)\n\n return {\n mounts: record.mounts,\n renders: renderCountRef.current,\n reset,\n }\n}\n","'use client'\n\nimport type { RenderCounterBadgeProps } from './types'\nimport { useRenderCounter } from './use-render-counter'\n\nconst POSITION_STYLES: Record<NonNullable<RenderCounterBadgeProps['position']>, string> = {\n 'top-right': 'top-2 right-2',\n 'top-left': 'top-2 left-2',\n 'bottom-right': 'bottom-2 right-2',\n 'bottom-left': 'bottom-2 left-2',\n}\n\n/**\n * A floating overlay badge displaying mount and render counters for debugging component lifecycles.\n */\nexport function RenderCounterBadge({\n id,\n position = 'top-right',\n className = '',\n style,\n logToConsole = false,\n disabled = false,\n}: RenderCounterBadgeProps) {\n const { mounts, renders } = useRenderCounter(id, { logToConsole, disabled })\n\n if (disabled) {\n return null\n }\n\n const posClass = POSITION_STYLES[position] || POSITION_STYLES['top-right']\n\n return (\n <div\n className={`absolute ${posClass} z-50 pointer-events-none flex items-center gap-2 rounded px-2 py-1 text-[11px] font-mono bg-zinc-900/90 text-zinc-200 border border-zinc-700/80 shadow-md backdrop-blur-xs select-none ${className}`}\n style={style}\n >\n <span>\n M: <strong className=\"text-emerald-400 font-semibold\">{mounts}</strong>\n </span>\n <span className=\"opacity-30\">|</span>\n <span>\n R: <strong className=\"text-indigo-400 font-semibold\">{renders}</strong>\n </span>\n </div>\n )\n}\n","'use client'\n\nimport type { RenderCounterFooterProps } from './types'\nimport { useRenderCounter } from './use-render-counter'\n\n/**\n * A container footer component for displaying mount and render counters at the bottom of widgets or panels.\n */\nexport function RenderCounterFooter({\n id,\n label,\n children,\n className = '',\n footerClassName = '',\n style,\n logToConsole = false,\n disabled = false,\n}: RenderCounterFooterProps) {\n const displayId = label || id || 'widget'\n const { mounts, renders } = useRenderCounter(id, { logToConsole, disabled })\n\n if (disabled) {\n return <>{children}</>\n }\n\n return (\n <div\n className={`h-full w-full flex flex-col overflow-hidden min-h-0 ${className}`}\n style={style}\n >\n {children && <div className=\"flex-1 overflow-hidden relative min-h-0\">{children}</div>}\n <div\n className={`bg-zinc-900/90 border-t border-zinc-800 px-3 py-1.5 flex items-center justify-between text-[11px] text-zinc-400 font-mono shrink-0 select-none ${footerClassName}`}\n >\n <span className=\"truncate max-w-[140px] text-zinc-300 font-semibold\">{displayId}</span>\n <div className=\"flex items-center gap-3\">\n <span>\n Mounts: <strong className=\"text-emerald-400 font-semibold\">{mounts}</strong>\n </span>\n <span className=\"opacity-20\">|</span>\n <span>\n Renders: <strong className=\"text-indigo-400 font-semibold\">{renders}</strong>\n </span>\n </div>\n </div>\n </div>\n )\n}\n"]}
|